diff --git a/usermods/DHT/DHT.cpp b/usermods/DHT/DHT.cpp index 2ed3dd0ace..5a4def08b5 100644 --- a/usermods/DHT/DHT.cpp +++ b/usermods/DHT/DHT.cpp @@ -20,6 +20,8 @@ #define DHTTYPE DHT_TYPE_21 #elif USERMOD_DHT_DHTTYPE == 22 #define DHTTYPE DHT_TYPE_22 +#else +#error Invalid USERMOD_DHT_DHTTYPE #endif // Connect pin 1 (on the left) of the sensor to +5V @@ -57,11 +59,11 @@ DHT_nonblocking dht_sensor(DHTPIN, DHTTYPE); -class UsermodDHT : public Usermod { +class UsermodDHT : public Usermod, public TemperatureSensor, public HumiditySensor { private: unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; - float humidity, temperature = 0; + float tempC = 0, humidity = 0, temperature = 0; bool initializing = true; bool disabled = false; #ifdef USERMOD_DHT_MQTT @@ -89,6 +91,8 @@ class UsermodDHT : public Usermod { #ifdef USERMOD_DHT_STATS nextResetStatsTime = millis() + 60*60*1000; #endif + pluginManager.registerTemperatureSensor(*this, "DHT"); + pluginManager.registerHumiditySensor(*this, "DHT"); } void loop() { @@ -112,13 +116,14 @@ class UsermodDHT : public Usermod { } #endif - float tempC; if (dht_sensor.measure(&tempC, &humidity)) { #ifdef USERMOD_DHT_CELSIUS temperature = tempC; #else temperature = tempC * 9 / 5 + 32; #endif + _isTemperatureValid = true; + _isHumidityValid = true; #ifdef USERMOD_DHT_MQTT // 10^n where n is number of decimal places to display in mqtt message. Please adjust buff size together with this constant @@ -168,6 +173,8 @@ class UsermodDHT : public Usermod { if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) { disabled = true; + _isTemperatureValid = false; + _isHumidityValid = false; } } @@ -242,8 +249,11 @@ class UsermodDHT : public Usermod { return USERMOD_ID_DHT; } + private: + float do_getTemperatureC() override { return tempC; } + float do_getHumidity() override { return humidity; } }; static UsermodDHT dht; -REGISTER_USERMOD(dht); \ No newline at end of file +REGISTER_USERMOD(dht); diff --git a/usermods/Temperature/Temperature.cpp b/usermods/Temperature/Temperature.cpp index c86b9e9842..ff3bdff5ec 100644 --- a/usermods/Temperature/Temperature.cpp +++ b/usermods/Temperature/Temperature.cpp @@ -141,6 +141,7 @@ void UsermodTemperature::setup() { } lastMeasurement = millis() - readingInterval + 10000; initDone = true; + pluginManager.registerTemperatureSensor(*this, _name); } void UsermodTemperature::loop() { @@ -165,11 +166,15 @@ void UsermodTemperature::loop() { if (now - lastTemperaturesRequest >= 750 /* 93.75ms per the datasheet but can be up to 750ms */) { readTemperature(); if (getTemperatureC() < -100.0f) { - if (++errorCount > 10) sensorFound = 0; + if (++errorCount > 10) { + sensorFound = 0; + _isTemperatureValid = false; + } lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms return; } errorCount = 0; + _isTemperatureValid = true; #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { @@ -380,4 +385,4 @@ static uint16_t mode_temperature() { static UsermodTemperature temperature; -REGISTER_USERMOD(temperature); \ No newline at end of file +REGISTER_USERMOD(temperature); diff --git a/usermods/Temperature/UsermodTemperature.h b/usermods/Temperature/UsermodTemperature.h index 555b57cf7a..4ffde065e8 100644 --- a/usermods/Temperature/UsermodTemperature.h +++ b/usermods/Temperature/UsermodTemperature.h @@ -16,7 +16,7 @@ #define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 #endif -class UsermodTemperature : public Usermod { +class UsermodTemperature : public Usermod, public TemperatureSensor { private: @@ -71,6 +71,7 @@ class UsermodTemperature : public Usermod { #ifndef WLED_DISABLE_MQTT void publishHomeAssistantAutodiscovery(); #endif + float do_getTemperatureC() override { return temperature; } static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); @@ -107,4 +108,3 @@ class UsermodTemperature : public Usermod { void appendConfigData() override; }; - diff --git a/usermods/UM_DummySensor/README.md b/usermods/UM_DummySensor/README.md new file mode 100644 index 0000000000..cd0754a97f --- /dev/null +++ b/usermods/UM_DummySensor/README.md @@ -0,0 +1,4 @@ +# Dummy usermod to simulate random sensor readings. + +Use `UM_PluginDemo` and `UM_DummySensor` together as an example for how direct interactions between +usermods can be realized through the plugin API. diff --git a/usermods/UM_DummySensor/UM_DummySensor.cpp b/usermods/UM_DummySensor/UM_DummySensor.cpp new file mode 100644 index 0000000000..7484be697e --- /dev/null +++ b/usermods/UM_DummySensor/UM_DummySensor.cpp @@ -0,0 +1,104 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#include "wled.h" +#include "PluginAPI/custom/DummySensor/DummySensor.h" + +//-------------------------------------------------------------------------------------------------- + +static constexpr char _name[] = "Dummy-Sensor"; + +/** Dummy usermod implementation that simulates random sensor readings. + */ +class UM_DummySensor : public Usermod, public DummySensor, public TemperatureSensor, public HumiditySensor +{ + + // ----- usermod functions ----- + + void setup() override + { + // register sensor plugins + pluginManager.registerTemperatureSensor(*this, _name); + pluginManager.registerHumiditySensor(*this, _name); + // this dummy can deliver readings instantly + _isTemperatureValid = true; + _isHumidityValid = true; + } + + void loop() override {} + + // ----- DummySensor (our own) custom plugin functions ----- + + void enableTemperatureSensor() override + { + if (!_isTemperatureSensorEnabled) + pluginManager.registerTemperatureSensor(*this, _name); + _isTemperatureSensorEnabled = true; + } + + void disableTemperatureSensor() override + { + if (_isTemperatureSensorEnabled) + pluginManager.unregisterTemperatureSensor(*this); + _isTemperatureSensorEnabled = false; + } + + bool isTemperatureSensorEnabled() override { return _isTemperatureSensorEnabled; } + + void enableHumiditySensor() override + { + if (!_isHumiditySensorEnabled) + pluginManager.registerHumiditySensor(*this, _name); + _isHumiditySensorEnabled = true; + } + + void disableHumiditySensor() override + { + if (_isHumiditySensorEnabled) + pluginManager.unregisterHumiditySensor(*this); + _isHumiditySensorEnabled = false; + } + + bool isHumiditySensorEnabled() override { return _isHumiditySensorEnabled; } + + // ----- TemperatureSensor plugin functions ----- + + /// @copydoc TemperatureSensor::do_getTemperatureC() + float do_getTemperatureC() override { return readTemperature(); } + + // ----- HumiditySensor plugin functions ----- + + /// @copydoc HumiditySensor::do_getHumidity() + float do_getHumidity() override { return readHumidity(); } + + // ----- internal processing functions ----- + + /// The dummy implementation to simulate temperature values (based on perlin noise). + float readTemperature() + { + const int32_t raw = perlin16(strip.now * 8) - 0x8000; + // simulate some random temperature around 20°C + return 20.0f + raw / 65535.0f * 30.0f; + } + + /// The dummy implementation to simulate humidity values (a sine wave). + float readHumidity() + { + const int32_t raw = beatsin16_t(1); + // simulate some random humidity between 10% and 90% + return 10.0f + raw / 65535.0f * 80.0f; + } + + // ----- member variables ----- + + bool _isTemperatureSensorEnabled = false; + bool _isHumiditySensorEnabled = false; +}; + +//-------------------------------------------------------------------------------------------------- + +static UM_DummySensor um_DummySensor; +REGISTER_USERMOD(um_DummySensor); +DEFINE_PLUGIN_API(DummySensor, um_DummySensor); diff --git a/usermods/UM_DummySensor/library.json b/usermods/UM_DummySensor/library.json new file mode 100644 index 0000000000..6ff42239d3 --- /dev/null +++ b/usermods/UM_DummySensor/library.json @@ -0,0 +1,4 @@ +{ + "name": "UM_DummySensor", + "build": { "libArchive": false } +} diff --git a/usermods/UM_PluginDemo/README.md b/usermods/UM_PluginDemo/README.md new file mode 100644 index 0000000000..f6682554e5 --- /dev/null +++ b/usermods/UM_PluginDemo/README.md @@ -0,0 +1,4 @@ +# Examples for writing custom plugins. + +Use `UM_PluginDemo` and `UM_DummySensor` together as an example for how direct interactions between +usermods can be realized through the plugin API. diff --git a/usermods/UM_PluginDemo/UM_PluginDemo.cpp b/usermods/UM_PluginDemo/UM_PluginDemo.cpp new file mode 100644 index 0000000000..1bc2a0c4db --- /dev/null +++ b/usermods/UM_PluginDemo/UM_PluginDemo.cpp @@ -0,0 +1,174 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#include "wled.h" +#include "PluginAPI/custom/DummySensor/DummySensor.h" + +// override this via PlatformIO with your custom LED / relay GPIO +#ifndef UM_PLUGIN_DEMO_RELAY_PIN +#define UM_PLUGIN_DEMO_RELAY_PIN 8 // Pin number of a relay or LED. +#endif + +extern uint16_t mode_static(void); + +//-------------------------------------------------------------------------------------------------- + +/** Example effect for processing the readings of a TemperatureSensor and a HumiditySensor plugin. + * It also shows how to access the custom API of a plugin, which may or may not be compiled into + * the WLED binary. + */ +uint16_t mode_Thermometer() +{ + SEGMENT.clear(); + + // try to get the specific API of the DummySensor + DummySensor *dummySensor = GET_PLUGIN_API(DummySensor); + if (dummySensor) + { + // fill the background with light yellow when we're connected to the DummySensor + SEGMENT.fill(0x080800); + // enable/disable its TemperatureSensor + if (SEGMENT.check3) + { + if (!dummySensor->isTemperatureSensorEnabled()) + dummySensor->enableTemperatureSensor(); + } + else + { + if (dummySensor->isTemperatureSensorEnabled()) + dummySensor->disableTemperatureSensor(); + } + // enable/disable its HumiditySensor + if (SEGMENT.check2) + { + if (!dummySensor->isHumiditySensorEnabled()) + dummySensor->enableHumiditySensor(); + } + else + { + if (dummySensor->isHumiditySensorEnabled()) + dummySensor->disableHumiditySensor(); + } + } + + // try to get an external Temperature- and HumiditySensor + TemperatureSensor *tempSensor = pluginManager.getTemperatureSensor(); + HumiditySensor *humSensor = pluginManager.getHumiditySensor(); + if (!tempSensor && !humSensor) + return mode_static(); + + // got a TemperatureSensor? --> draw its reading as a bar + if (tempSensor && tempSensor->isReady()) + { + // read the temperature value from the plugin + const float temp = tempSensor->temperatureC(); + // and draw a representing bar in the FX color + int tempPos = temp * (SEGLEN - 1) / 40.0f; // 20° shall be in the middle + while (tempPos >= 0) + SEGMENT.setPixelColor(tempPos--, fast_color_scale(SEGCOLOR(0), 64)); + } + + // draw some dots as scale + SEGMENT.setPixelColor(SEGLEN / 4, 0x8080F8); // 10°C + SEGMENT.setPixelColor(SEGLEN / 2, 0x808080); // 20°C + SEGMENT.setPixelColor(SEGLEN * 3 / 4, 0xF88080); // 30°C + + // got a HumiditySensor? --> draw its reading as a dot + if (humSensor && humSensor->isReady()) + { + // read the humidity value from the plugin + const float hum = humSensor->humidity(); + // and draw a representing dot in magenta + const int humPos = hum * (SEGLEN - 1) / 100.0f; + SEGMENT.setPixelColor(humPos, 0xFF00FF); + } + + return FRAMETIME; +} +static const char _data_FX_MODE_EX_UM_THERMOMETER[] PROGMEM = "Ex: Thermometer@,,,,,,Humidity Dummy,Temperature Dummy;!;;o2=1,o3=1"; + +//-------------------------------------------------------------------------------------------------- + +static constexpr char _name[] = "Demo-Plugin"; + +/** A usermod as plugin example. + */ +class UM_PluginDemo : public Usermod, public PinUser +{ + // ----- usermod functions ----- + + void setup() override + { + registerEffects(); + registerPins(); + } + + void loop() override + { + processFanControl(); + } + + // ----- initialization helper functions ----- + + void registerEffects() + { + strip.addEffect(255, &mode_Thermometer, _data_FX_MODE_EX_UM_THERMOMETER); + } + + void registerPins() + { + // for single pin configuration: + // _pinConfig.pinNr = UM_PLUGIN_DEMO_RELAY_PIN; + _pinConfig[0].pinNr = UM_PLUGIN_DEMO_RELAY_PIN; + if (pluginManager.registerPinUser(*this, _pinConfig, _name)) + { + pinMode(_pinConfig[0].pinNr, OUTPUT); + } + } + + // ----- internal processing functions ----- + + /// Just an example for custom plugin sensor data processing. + void processFanControl() + { + // quit if we didn't get a GPIO + if (!_pinConfig[0].isPinValid()) + return; + + // try to get a TemperatureSensor - and quit if that fails + TemperatureSensor *tempSensor = pluginManager.getTemperatureSensor(); + if (!tempSensor || !tempSensor->isReady()) + return; + + // read the temperature value from the other plugin + const float temp = tempSensor->temperatureC(); + // and control a connected fan via relay + const bool isHot = temp > 20.0f; + digitalWrite(_pinConfig[0].pinNr, isHot ? HIGH : LOW); + } + + // ----- member variables ----- + + PinConfigs<12> _pinConfig{{{PinType::Digital_out, "Relay"}, + // the following are just demo examples: + {PinType::Digital_in}, + {PinType::Digital_out}, + {PinType::PWM_out}, + {PinType::I2C_scl}, + {PinType::I2C_sda}, + {PinType::OneWire}, + {PinType::OneWire, "1W-aux"}, // 2nd bus with different name + {PinType::SPI_miso}, + {PinType::SPI_mosi}, + {PinType::SPI_sclk}, + {PinType::Analog_in}}}; + // how a single pin configuration would look like: + // PinConfig _pinConfig{PinType::Digital_out, "Relay"}; +}; + +//-------------------------------------------------------------------------------------------------- + +static UM_PluginDemo um_PluginDemo; +REGISTER_USERMOD(um_PluginDemo); diff --git a/usermods/UM_PluginDemo/library.json b/usermods/UM_PluginDemo/library.json new file mode 100644 index 0000000000..b6927ed5c9 --- /dev/null +++ b/usermods/UM_PluginDemo/library.json @@ -0,0 +1,4 @@ +{ + "name": "UM_PluginDemo", + "build": { "libArchive": false } +} diff --git a/wled00/PluginAPI/DevNotes.md b/wled00/PluginAPI/DevNotes.md new file mode 100644 index 0000000000..7aee086453 --- /dev/null +++ b/wled00/PluginAPI/DevNotes.md @@ -0,0 +1,42 @@ + +# Development Notes on Plugins + +The igniting spark for this new WLED feature came from [this issue](https://github.com/wled/WLED/issues/5290) by @blazoncek , and some comments from @softhack007 and @willmmiles in the discussion there. Kudos to them for the great ideas! + +These notes describe why some parts of this plugin design are as they are. And which problems during development led to those design decisions. + + +## Why having a dedicated API for a usermod; why not using the usermod class directly by other usermods? + +(1) To break dependencies. Imagine, your usermod wants to call functions on another usermod, that for examples controls a display via I2C. The class definition (yet not the implementation) of that other usermod would have to be inside its headerfile, which your usermod must include.
+Now, since that usermod uses I2C, it will depend on any kind of I2C library, and have an 'I2C-driver' as member variable. As a consequence, your usermod will implicitly pull in all these dependencies of the other usermod. This is _working only if the other usermod is also enabled_, so its dependencies are getting installed automatically by PlatformIO, and the include paths are set appropriately by the build system.
+The problem arises, when your usermod wants to access the other usermod _optionally_ - i.e. only when it has been compiled into WLED. If it is missing, you want to detect that inside your usermod at runtime, and maybe just skip the part with the display interaction. You'll end up with a compiler error: your usermod includes the other's headerfile, which in turn includes the I2C driver's headerfile. Unfortunately, that I2C driver is not installed (and won't be), because no one else needed it yet. So, the compiler quits because of a missing headerfile - Game Over.
+ +(2) An alternative solution would be to use `#define`s, which are set by the other usermod. Ideas about how to do this have already been pointed out in the issue's thread by @softhack007 . However, that approach does _not_ work out of the box, because of some magic performed by the Arduino framework; see below for more details on that problem. + +(3) See the failing build as a feature and not as a problem. Just include the other's headerfile in your usermod, and use it like any other class. Therefore, make its object accessible through its headerfile. (How to do that has also been shown in the issue's thread by @willmmiles )
+Now, when the build fails, this is a reminder for you that you forgot to enable the other usermod as well. But as a consequence, the 'optional' part of detecting the other usermod is then gone. + +In the end, it will be the developer's choice which option to choose; there is not only one right way. This PR shows an additional clean and interface-based way - at the expense of virtual functions. However, when weighing the cost of the vtable against the feature gain and simplicity, I am happily willing to pay that price. + + +## Why a dedicated folder for custom usermod APIs - shouldn't they live in the same folder as the usermod itself? + +Short answer: Yes, absolutely! Long answer: Unfortunately that doesn't work in practice. +The problem here is some magic that the underlying Arduino framework (I assume) is performing under the hood:
+Whenever your sourcecode includes a headerfile from the directory of another usermod, that usermod's cpp files will be compiled automatically. Regardless if that usermod is enabled or not! This always ends with a failed build in the latter case, because e.g. include paths are not set appropriately. At least that was my experience during development. If anyone knows how to prevent that from happening: solutions are really welcome! + + +## Why does a custom usermod API need a cpp file, when it is just an interface? + +This is necessary because of the way _weak symbols_ are handled by the linker. At least how far I understand them as of now, after some contradictory adventures with AI. Nevertheless, this idea was with weak linking was brought up by @softhack007 in the issue's thread - and was the initial spark for me to start tinkering on that topic; just out of curiosity.
+My experience during development was that weak symbols are **not** NULL when they are missing. It seems as if they are undefined (i.e. random), which leads to a crash. Apparently, we need an additional _weak implementation_ of such a _weak symbol_, which is initialized with NULL (that's happening inside this cpp file). When the (optional) usermod is missing, that _weak implementation_ is picked by the linker, and can be checked for NULL by the user.
+Now, when the actual usermod is enabled, it contains a _strong implementation_ of that _weak symbol_ - which the linker will pick instead of the weak one from this cpp file. Thus the user obtained a valid pointer to the usermod object (not NULL), and can now directly interact with it.
+If anyone knows a solution how the linker will default missing weak symbols to NULL, please share! Then we can get rid of this cpp file and the corresponding macro completely! + + +## The PluginManager's info section in the UI is a bit bloated and messy... + +Yes, indeed... The current implementation is intended as demonstration and for debugging. +For release code, it may also be removed completely since it isn't essentially needed. +My idea would be to make its 'bloatiness' configurable via `#define`s, like `PLUGINMGR_DISABLE_UI`. Any suggestions about what would be of interest are highly appreciated! diff --git a/wled00/PluginAPI/HumiditySensor.h b/wled00/PluginAPI/HumiditySensor.h new file mode 100644 index 0000000000..1c70304101 --- /dev/null +++ b/wled00/PluginAPI/HumiditySensor.h @@ -0,0 +1,28 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#pragma once + +//-------------------------------------------------------------------------------------------------- + +/// Interface of a sensor that can provide humidity readings. +class HumiditySensor +{ +public: + /// Returns \c false when the sensor is not ready. + bool isReady() { return _isHumidityValid; } + + /// Get the humidity in % rel. + float humidity() { return do_getHumidity(); } + +protected: + /// Get the plugin's humidity reading in % rel. + virtual float do_getHumidity() = 0; + + /// The plugin must set this to \c true after the sensor has been initialized. + bool _isHumidityValid = false; +}; + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/PinUser.h b/wled00/PluginAPI/PinUser.h new file mode 100644 index 0000000000..44e3f197a3 --- /dev/null +++ b/wled00/PluginAPI/PinUser.h @@ -0,0 +1,83 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#pragma once + +#include + +//-------------------------------------------------------------------------------------------------- + +/// Type of GPIO pin. +enum class PinType : uint8_t +{ + undefined = 0, + Digital_in, + Digital_out, + Analog_in, + PWM_out, + I2C_scl, + I2C_sda, + SPI_sclk, + SPI_mosi, + SPI_miso, + OneWire +}; + +const char *getPinName(PinType pinType); + +/** Properties of a GPIO pin that a plugin wants to use. + * @note With the current design of WLED, the PinUser (i.e. the usermod) is responsible for + * obtaining the pin numbers from the UI. This "wanted pin to use" shall be specified here. + * In a future optimization, this burden can be eliminated completely: \n + * The PinUser leaves the pin number uninitialized. An appropriate pin number will be assigned by + * the PluginManager upon registration of the PinUser. All the UI interaction will then be under + * full control of the PluginManager; a PinUser won't have to care about that anymore. \n + * Changes in the pin configuration (via UI) will be stored directly inside the PinUser's PinConfig + * (since the PluginManager keeps track of them), and announced via \c onPinConfigurationChanged() + * This callback acts as a trigger for the PinUser to re-initialize with the updated pin numbers + * from inside its PinConfig. + */ +struct PinConfig +{ + PinConfig(PinType pinType_ = PinType::undefined, const char *pinName_ = nullptr) + : pinType{pinType_}, pinName{pinName_} {} + + /// The designated pin number. + uint8_t pinNr = 0xFF; + + /// Type of the requested pin. + PinType pinType; + + /** Name of the pin (optional). + * To be displayed in UI configuration page; it is derived from \c pinType when omitted. + * @note Pin names are not deeply copied. + * They must remain valid as long as the plugin is registered. + */ + const char *pinName; + + /** Pins are marked as invalid when the plugin registration fails. + * @note The PinUser must then assign a different pin number and try to register again. + */ + bool isPinValid() const { return pinNr != 0xFF; } + void invalidatePin() { pinNr = 0xFF; } +}; + +/// Array with multiple pin configurations. +template +using PinConfigs = std::array; + +/// Interface of a plugin that wants to use GPIO pins. +class PinUser +{ +#if (0) // only with PluginManager's UI optimization for pin selection + /// The pin configuration (from the UI) has changed. Not implemented yet. + virtual void onPinConfigurationChanged() {} +#else + // Intentionally empty. + // Any pin users must just have this as base class. +#endif +}; + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/PluginMacros.h b/wled00/PluginAPI/PluginMacros.h new file mode 100644 index 0000000000..4c6f876d11 --- /dev/null +++ b/wled00/PluginAPI/PluginMacros.h @@ -0,0 +1,68 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#pragma once + +//-------------------------------------------------------------------------------------------------- + +/** Place this macro in the headerfile of the custom plugin API. + * Argument \a API_type is the class name of the plugin's interface: + * @code + * // in PluginAPI/custom/MyStuff/MyStuff.h + * #include "PluginAPI/PluginMacros.h" + * class MyStuff + * { + * public: + * virtual void doSomething() = 0; + * }; + * DECLARE_PLUGIN_API(MyStuff); + * @endcode + */ +#define DECLARE_PLUGIN_API(API_type) extern API_type *PLUGINAPI_##API_type __attribute__((weak)) + +/** Place this macro in the cpp file of the custom plugin API. + * @note Every API needs its own source file, which must always be compiled; regardless whether the + * corresponding usermod is enabled or not. It usually contains just the few lines that are shown + * below. Be aware that the header and the cpp file of the API \e must be located in a different + * directory than the actual usermod implementation! + * @code + * // in PluginAPI/custom/MyStuff/MyStuff.cpp + * #include "MyStuff.h" + * CPPFILE_PLUGIN_API(MyStuff); + * @endcode + */ +#define CPPFILE_PLUGIN_API(API_type) API_type *PLUGINAPI_##API_type __attribute__((weak)) = nullptr + +/** Place this macro in the cpp file of the corresponding usermod implementation. + * @note The usermod itself doesn't necessarily need to have its own headerfile; everything may be + * contained inside its cpp file. + * @code + * // in usermods/UM_MyStuff/UM_MyStuff.cpp + * #include "PluginAPI/custom/MyStuff/MyStuff.h" + * class UM_MyStuff : public MyStuff + * { + * void doSomething() override { ... } + * }; + * static UM_MyStuff um_MyStuff; + * REGISTER_USERMOD(um_MyStuff); + * DEFINE_PLUGIN_API(MyStuff, um_MyStuff); + * @endcode + */ +#define DEFINE_PLUGIN_API(API_type, instance) API_type *PLUGINAPI_##API_type = &instance + +/** Use this macro at all places where you want to interact through the plugin API. + * @code + * // in usermods/UM_AnotherUsermod/UM_AnotherUsermod.cpp + * #include "PluginAPI/custom/MyStuff/MyStuff.h" + * // ... + * MyStuff* myStuff = GET_PLUGIN_API(MyStuff); + * if (myStuff) + * myStuff->doSomething(); + * // else: WLED was compiled without the corresponding usermod + * @endcode + */ +#define GET_PLUGIN_API(API_type) PLUGINAPI_##API_type + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/PluginManager.cpp b/wled00/PluginAPI/PluginManager.cpp new file mode 100644 index 0000000000..0b73e1c9a0 --- /dev/null +++ b/wled00/PluginAPI/PluginManager.cpp @@ -0,0 +1,424 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#include +#include +#include "wled.h" +#include "PluginManager.h" + +//-------------------------------------------------------------------------------------------------- + +namespace +{ + + static const char *unknownName = "[???]"; + static const char *notAvailable = "[n/a]"; + +#ifdef PLUGINMGR_GROUP_UI_INFO_BY_NAME + class InfoDataBuilder + { + using DataMap = std::map; + + public: + void add(const char *key, const String &value) + { + auto &entry = _dict[key]; + if (!entry.isEmpty()) + entry += "
"; + entry += value; + } + + using const_iterator = DataMap::const_iterator; + const_iterator begin() const { return _dict.begin(); } + const_iterator end() const { return _dict.end(); } + + private: + DataMap _dict; + }; +#endif // PLUGINMGR_GROUP_UI_INFO_BY_NAME + + bool isOutputPin(PinType pinType) + { + switch (pinType) + { + // case PinType::Digital_in: + case PinType::Digital_out: + // case PinType::Analog_in: + case PinType::PWM_out: + case PinType::I2C_scl: + case PinType::I2C_sda: + case PinType::SPI_sclk: + case PinType::SPI_mosi: + // case PinType::SPI_miso: + case PinType::OneWire: + return true; + default: + return false; + } + } + +} + +//-------------------------------------------------------------------------------------------------- + +const char *getPinName(PinType pinType) // only needed for allocations at PinManager +{ + switch (pinType) + { + case PinType::Digital_in: + return "Digital in"; + case PinType::Digital_out: + return "Digital out"; + case PinType::Analog_in: + return "Analog in"; + case PinType::PWM_out: + return "PWM out"; + case PinType::I2C_scl: + return "I2C SCL"; + case PinType::I2C_sda: + return "I2C SDA"; + case PinType::SPI_sclk: + return "SPI SCLK"; + case PinType::SPI_mosi: + return "SPI MOSI"; + case PinType::SPI_miso: + return "SPI MISO"; + case PinType::OneWire: + return "OneWire"; + default: + return unknownName; + } +} + +//-------------------------------------------------------------------------------------------------- + +// ----- PinUser handling ----- + +bool PluginManager::registerPinUser(PinUser &user, uint8_t pinCount, PinConfig *pinConfig, const char *pluginName) +{ + unregisterPinUser(user); // _always_ remove previous registration (even on invalid args) + if (pinCount == 0 || pinConfig == nullptr) + return false; + + auto begin = pinConfig; + auto end = begin + pinCount; + for (auto itr = begin; itr != end; ++itr) + { + if (itr->pinName == nullptr) + itr->pinName = getPinName(itr->pinType); + if (!itr->isPinValid()) + { + // this is just a demo example how auto-pin-assignment could look like + // (no interaction with PinManger in that case) + static uint8_t counter = 199; + itr->pinNr = ++counter; // this should be determined by some kind of future "ResourceManager" + // TODO(cleanup) Enable this rollback again before production + // return rollbackPinRegistration(user, pinCount, pinConfig); + } + else if (!PinManager::allocatePin(itr->pinNr, isOutputPin(itr->pinType), PinOwner::PluginMgr)) + return rollbackPinRegistration(user, pinCount, pinConfig); + _pinUserConfigs.emplace_back(&user, *itr); + } + + _pinUsers.emplace_back(&user, pluginName); + return true; +} + +bool PluginManager::rollbackPinRegistration(PinUser &user, uint8_t pinCount, PinConfig *pinConfig) +{ + unregisterPinUser(user); + auto begin = pinConfig; + auto end = begin + pinCount; + for (auto itr = begin; itr != end; ++itr) + itr->invalidatePin(); + return false; +} + +void PluginManager::unregisterPinUser(PinUser &user) +{ + for (auto itr = _pinUserConfigs.begin(); itr != _pinUserConfigs.end();) + { + const auto &pinUser = itr->first; + if (pinUser == &user) + { + const auto &pinConfig = itr->second; + PinManager::deallocatePin(pinConfig.pinNr, PinOwner::PluginMgr); + itr = _pinUserConfigs.erase(itr); + } + else + { + ++itr; + } + } + + auto pred = [&user](const PinUsers::value_type &entry) + { return entry.first == &user; }; + _pinUsers.erase( + std::remove_if(_pinUsers.begin(), _pinUsers.end(), pred), _pinUsers.end()); +} + +// ----- TemperatureSensor handling ----- + +void PluginManager::registerTemperatureSensor(TemperatureSensor &sensor, const char *pluginName) +{ + unregisterTemperatureSensor(sensor); + _temperatureSensors.emplace_back(&sensor, pluginName); +} + +void PluginManager::unregisterTemperatureSensor(TemperatureSensor &sensor) +{ + // https://en.wikipedia.org/wiki/Erase%E2%80%93remove_idiom + auto pred = [&sensor](const TemperatureSensors::value_type &entry) + { return entry.first == &sensor; }; + _temperatureSensors.erase( + std::remove_if(_temperatureSensors.begin(), _temperatureSensors.end(), pred), _temperatureSensors.end()); +} + +// ----- HumiditySensor handling ----- + +void PluginManager::registerHumiditySensor(HumiditySensor &sensor, const char *pluginName) +{ + unregisterHumiditySensor(sensor); + _humiditySensors.emplace_back(&sensor, pluginName); +} + +void PluginManager::unregisterHumiditySensor(HumiditySensor &sensor) +{ + auto pred = [&sensor](const HumiditySensors::value_type &entry) + { return entry.first == &sensor; }; + _humiditySensors.erase( + std::remove_if(_humiditySensors.begin(), _humiditySensors.end(), pred), _humiditySensors.end()); +} + +// ----- name handling ----- + +const char *PluginManager::getSensorName(const TemperatureSensor *sensor) const +{ + auto pred = [sensor](const TemperatureSensors::value_type &entry) + { return entry.first == sensor; }; + const auto itr = std::find_if(_temperatureSensors.begin(), _temperatureSensors.end(), pred); + return itr == _temperatureSensors.end() ? unknownName : itr->second; +} + +const char *PluginManager::getSensorName(const HumiditySensor *sensor) const +{ + auto pred = [sensor](const HumiditySensors::value_type &entry) + { return entry.first == sensor; }; + const auto itr = std::find_if(_humiditySensors.begin(), _humiditySensors.end(), pred); + return itr == _humiditySensors.end() ? unknownName : itr->second; +} + +const char *PluginManager::getPluginName(const PinUser *user) const +{ + auto pred = [user](const PinUsers::value_type &entry) + { return entry.first == user; }; + const auto itr = std::find_if(_pinUsers.begin(), _pinUsers.end(), pred); + return itr == _pinUsers.end() ? unknownName : itr->second; +} + +// ----- UI interaction ----- + +#ifndef PLUGINMGR_DISABLE_UI + +void PluginManager::addToJsonInfo(JsonObject &root, bool advanced) +{ + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + addUiInfo_plugins(user); +#ifdef PLUGINMGR_GROUP_UI_INFO_BY_NAME + addUiInfo(user, advanced); +#else + addUiInfo_basic(user); + if (advanced) + addUiInfo_advanced(user); +#endif // PLUGINMGR_GROUP_UI_INFO_BY_NAME +} + +#ifdef PLUGINMGR_GROUP_UI_INFO_BY_NAME + +void PluginManager::addUiInfo(JsonObject &user, bool advanced) +{ + InfoDataBuilder info; + + for (const auto &entry : _temperatureSensors) + { + auto &sensor = *entry.first; + String val; + val += "Temperature = "; + if (sensor.isReady()) + { + val += sensor.temperature(); + val += sensor.useFahrenheit() ? " °F" : " °C"; + } + else + { + val += notAvailable; + } + info.add(entry.second, val); + } + + for (const auto &entry : _humiditySensors) + { + auto &sensor = *entry.first; + String val; + val += "Humidity = "; + if (sensor.isReady()) + { + val += sensor.humidity(); + val += " %rel"; + } + else + { + val += notAvailable; + } + info.add(entry.second, val); + } + + if (advanced) + { + for (const auto &entry : _pinUserConfigs) + { + const PinConfig &config = entry.second; + String val; + val += config.pinName; + val += " = Pin "; + val += config.pinNr; + info.add(getPluginName(entry.first), val); + } + } + + for (const auto &line : info) + user.createNestedArray(line.first).add(line.second); +} + +#else // PLUGINMGR_GROUP_UI_INFO_BY_NAME + +void PluginManager::addUiInfo_basic(JsonObject &user) +{ + int counter = 0; + for (const auto &entry : _temperatureSensors) + { +#if (0) + TemperatureSensor &sensor = *entry.first; + String key; + key += "Temp. "; + key += ++counter; + key += " = "; + if (sensor.isReady()) + { + key += sensor.temperature(); + key += sensor.useFahrenheit() ? " °F" : " °C"; + } + else + { + key += notAvailable; + } + user.createNestedArray(key).add(entry.second); +#else + TemperatureSensor &sensor = *entry.first; + String key; + key += "Temp. "; + key += ++counter; + key += ": "; + key += entry.second; + String val; + if (sensor.isReady()) + { + val += sensor.temperature(); + val += sensor.useFahrenheit() ? " °F" : " °C"; + } + else + { + val += notAvailable; + } + user.createNestedArray(key).add(val); +#endif + } + + counter = 0; + for (const auto &entry : _humiditySensors) + { +#if (0) + HumiditySensor &sensor = *entry.first; + String key; + key += "Hum. "; + key += ++counter; + key += " = "; + if (sensor.isReady()) + { + key += sensor.humidity(); + key += " %rel"; + } + else + { + key += notAvailable; + } + user.createNestedArray(key).add(entry.second); +#else + HumiditySensor &sensor = *entry.first; + String key; + key += "Hum. "; + key += ++counter; + key += ": "; + key += entry.second; + String val; + if (sensor.isReady()) + { + val += sensor.humidity(); + val += " %rel"; + } + else + { + val += notAvailable; + } + user.createNestedArray(key).add(val); +#endif + } +} + +void PluginManager::addUiInfo_advanced(JsonObject &user) +{ + for (const auto &entry : _pinUserConfigs) + { +#if (0) + const PinConfig &config = entry.second; + String key; + key += "GPIO "; + key += config.pinNr; + key += " = "; + key += config.pinName; + user.createNestedArray(key).add(getPluginName(entry.first)); +#else + const PinConfig &config = entry.second; + String key; + key += "GPIO "; + key += config.pinNr; + key += ": "; + key += getPluginName(entry.first); + user.createNestedArray(key).add(config.pinName); +#endif + } +} + +#endif // PLUGINMGR_GROUP_UI_INFO_BY_NAME + +void PluginManager::addUiInfo_plugins(JsonObject &user) +{ + // JsonArray line = user.createNestedArray("
Hello"); + // line.add("
from "); + // line.add("
Plugins!"); +} + +#endif // PLUGINMGR_DISABLE_UI + +//-------------------------------------------------------------------------------------------------- + +bool TemperatureSensor::_useFahrenheit = false; + +PluginManager pluginManager; + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/PluginManager.h b/wled00/PluginAPI/PluginManager.h new file mode 100644 index 0000000000..bd362e7f3a --- /dev/null +++ b/wled00/PluginAPI/PluginManager.h @@ -0,0 +1,120 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#pragma once + +#include + +#include "PinUser.h" +#include "HumiditySensor.h" +#include "TemperatureSensor.h" + +// set this via PlatformIO to disable PluginManager's UI info entries for demo & debugging +// #define PLUGINMGR_DISABLE_UI + +// set this via PlatformIO for grouping PluginManager's UI info entries by plugin name +// #define PLUGINMGR_GROUP_UI_INFO_BY_NAME + +//-------------------------------------------------------------------------------------------------- + +/** The central component that orchestrates all plugins. + * @note All strings (names) that are provided from the plugins are only pointer-copied. + * They must remain valid as long as the plugin is registered! + */ +class PluginManager +{ +public: + // no copy & move + PluginManager(const PluginManager &) = delete; + PluginManager &operator=(const PluginManager &) = delete; + PluginManager() = default; + + // ----- PinUser handling ----- + + /** Try to register the desired pins of a plugin. + * @note Use this method also for updating the pin configuration (even if already registered). + */ + bool registerPinUser(PinUser &user, uint8_t pinCount, PinConfig *pinConfig, const char *pluginName); + + /// Convenience method for registering one single pin. + bool registerPinUser(PinUser &user, PinConfig &pinConfig, const char *pluginName) + { + return registerPinUser(user, 1, &pinConfig, pluginName); + } + + /// Convenience method for registering an array of pins. + template + bool registerPinUser(PinUser &user, std::array &pinConfig, const char *pluginName) + { + return registerPinUser(user, NUM_PINS, pinConfig.data(), pluginName); + } + + /// Revoke all of a plugin's registered pins. + void unregisterPinUser(PinUser &user); + + // ----- Sensor handling ----- + + // Register a plugin as a specific sensor. + void registerTemperatureSensor(TemperatureSensor &sensor, const char *pluginName); + void registerHumiditySensor(HumiditySensor &sensor, const char *pluginName); + + // Revoke a plugin's registration as specific sensor. + void unregisterTemperatureSensor(TemperatureSensor &sensor); + void unregisterHumiditySensor(HumiditySensor &sensor); + + // Get a registered sensor, or nullptr if there is none. + TemperatureSensor *getTemperatureSensor(uint8_t index = 0) { return index < _temperatureSensors.size() ? _temperatureSensors[index].first : nullptr; } + HumiditySensor *getHumiditySensor(uint8_t index = 0) { return index < _humiditySensors.size() ? _humiditySensors[index].first : nullptr; } + + size_t getTemperatureSensorCount() const { return _temperatureSensors.size(); } + size_t getHumiditySensorCount() const { return _humiditySensors.size(); } + + const char *getSensorName(const TemperatureSensor *sensor) const; + const char *getSensorName(const HumiditySensor *sensor) const; + + // ----- WLED internal stuff ----- + + /** Globally set the unit for \c TemperatureSensor::temperature() - default is °C + * @note This setting should only be configured via UI, and not via usermod or effect. + */ + static void setUseFahrenheit(bool enabled) { TemperatureSensor::_useFahrenheit = enabled; } + +#ifndef PLUGINMGR_DISABLE_UI + void addToJsonInfo(JsonObject &root, bool advanced = true); +#else + void addToJsonInfo(JsonObject &root, bool advanced = true) {} +#endif + +private: + bool rollbackPinRegistration(PinUser &user, uint8_t pinCount, PinConfig *pinConfig); +#ifndef PLUGINMGR_DISABLE_UI +#ifdef PLUGINMGR_GROUP_UI_INFO_BY_NAME + void addUiInfo(JsonObject &root, bool advanced); +#else + void addUiInfo_basic(JsonObject &user); + void addUiInfo_advanced(JsonObject &user); +#endif // PLUGINMGR_GROUP_UI_INFO_BY_NAME + void addUiInfo_plugins(JsonObject &user); +#endif // PLUGINMGR_DISABLE_UI + const char *getPluginName(const PinUser *user) const; + + // TODO(optimization) To save precious DRAM, use std::pmr::vector with a memory resource that + // allocates PSRAM instead. Unfortunately, that is a C++17 feature... + using PinUsers = std::vector>; + using PinUserConfigs = std::vector>; // yes, we store a _copy_ of the config! + PinUsers _pinUsers; + PinUserConfigs _pinUserConfigs; + + using TemperatureSensors = std::vector>; + TemperatureSensors _temperatureSensors; + + using HumiditySensors = std::vector>; + HumiditySensors _humiditySensors; +}; + +/// The global PluginManager instance. +extern PluginManager pluginManager; + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/README.md b/wled00/PluginAPI/README.md new file mode 100644 index 0000000000..012ba08837 --- /dev/null +++ b/wled00/PluginAPI/README.md @@ -0,0 +1,222 @@ +# A Plugin Framework for WLED's Usermods. + +**TL;DR**
+These interfaces here are very high-level by explicit design choice - and they are administrated +by the WLED framework only.
+This directory here is **not** the place for specialized custom interfaces - use the subdirectory +`custom` for that.
+Have a look at the examples and the comments in the source files for inspiration for your own +usermods. + + +## Upgrading Usermods to Plugins + +Read the design description below, follow the examples in the usermods `UM_PluginDemo` and +`UM_DummySensor`, and have a look at the comments in the source files inside this directory. + + +# Design Description + +## Administrated Plugin APIs + +These interfaces are administrated by WLED, which means that they shall remain very stable and +generic. All usermods can rely on the assumption that these won't change (at least not a lot and not +breaking). The `PluginManager` inside WLED is aware of all instances of these interfaces, and will +establish the connections between all users and providers of them. + +### The Problem + +A usermod contains an effect, which wants to render the current temperature in °C as a bar on the +LED strip. Therefore it needs a temperature sensor that it can ask for the actual value: + +```mermaid +classDiagram + direction LR + class UM_PluginDemo { <> + mode_Thermometer() + } + class TemperatureSensor ["Please, anyone??"] { + +temperatureC() + } + + UM_PluginDemo ..> TemperatureSensor +``` + +### The Solution + +WLED defines an interface, which is designated for providing current temperature readings. Our +desperate usermod is now happy and can draw whatever it feels to, based on the data from the +sensor.
+However, another usermod will also be needed, which implements that interface. It contains the +actual magic of reading meaningful data from a small piece of hardware.
+Enter: The `DHT` usermod: + +```mermaid +classDiagram + direction LR + class UM_PluginDemo { <> + mode_Thermometer() + } + class TemperatureSensor { <> + +temperatureC() + #do_getTemperatureC()* + } + class DHT { <> + -do_getTemperatureC() + } + + UM_PluginDemo --> TemperatureSensor + TemperatureSensor <|.. DHT +``` + +### More Solutions + +Since there exist more real temperature sensors than the `DHT` usermod can handle, there are other +usermods for other hardware. And even a `DummySensor`, which just simulates temperature readings.
+Regardless of which one of those other usermods is compiled into WLED, our little thermometer +effect will always get its desired temperature value. Without actually having to worry about from +whom (because the `PluginManager` takes care of that): + +```mermaid +classDiagram + direction LR + class UM_PluginDemo { <> + mode_Thermometer() + } + class TemperatureSensor { <> + +temperatureC() + #do_getTemperatureC()* + } + class DHT { <> + -do_getTemperatureC() + } + class Temperature { <> + -do_getTemperatureC() + } + class UM_DummySensor { <> + -do_getTemperatureC() + } + + UM_PluginDemo --> TemperatureSensor + TemperatureSensor <|.. DHT + TemperatureSensor <|.. Temperature + TemperatureSensor <|.. UM_DummySensor +``` + + +## Custom Plugin APIs + +Our `DummySensor` can offer more functionality through a custom interface, which is defined by the +usermod itself (and not by WLED, in contrast to the administrated interfaces from before). Through +that custom interface, it can offer any kind of functionality (whether it makes sense for anyone +else or not). In our example, its simulated TemperatureSensor can be enabled and disabled.
+Just out of fun, our thermometer effect forwards the state of a checkbox in the UI to those methods. +This really doesn't make any sense, but it shows nicely how a custom plugin API is working. + +```mermaid +classDiagram + direction LR + class UM_PluginDemo { <> + Checkbox_in_UI + mode_Thermometer() + } + class TemperatureSensor { <> + +temperatureC() + #do_getTemperatureC()* + } + class DHT { <> + -do_getTemperatureC() + } + class Temperature { <> + -do_getTemperatureC() + } + class DummySensor { <> + +enableTemperatureSensor() + +disableTemperatureSensor() + #do_enableTemperatureSensor()* + #do_disableTemperatureSensor()* + } + class UM_DummySensor { <> + -do_getTemperatureC() + -do_enableTemperatureSensor() + -do_disableTemperatureSensor() + } + + UM_PluginDemo --> TemperatureSensor + UM_PluginDemo --> DummySensor + TemperatureSensor <|.. DHT + TemperatureSensor <|.. Temperature + TemperatureSensor <|.. UM_DummySensor + DummySensor <|.. UM_DummySensor + + note for TemperatureSensor "Administrated by WLED." + note for DummySensor "Administrated by the usermod." +``` + +To make things complete, the thermometer effect can easily detect if WLED has been compiled with or +without the `DummySensor` usermod. In case the dummy is missing, it will just ignore the checkbox. + + +## GPIO Pin Handling for Plugins + +Usermods can acquire their desired GPIO pins through the `PluginManager`. For an example, have a +look at the `UM_PluginDemo` usermod.
+In a nutshell, the procedure is as follows: +- Usermods that want to use GPIO pins must have `PinUser` as a base class. +- They specify their wanted pins through an instance (or array) of `PinConfig` member variable. + - This contains the desired pin number, pin functionality, and optionally a name for the pin. +- During `setup()`, they register this configuration at the `PluginManager`. +- The `PluginManager` forwards the request to WLED's `PinManager` and returns its result. + +There's no longer a need for defining dedicated `PinManager` constants for every usermod inside +`pin_manager.h`. All registrations are done in the name of the `PluginManager` (as a proxy), who +internally keeps track of all its registered `PinUser`s. + + +# Appendix + +## Custom Builds with Usermods + +For everyone who is frequently forgetting how to enable usermods for custom WLED builds (like me): +Here's your friendly reminder! + +Create a file `platformio_override.ini` in the root directory, and paste the following code into +that file. Probably you have to restart VS Code to make that new entry appear in the PlatformIO +Project Environment selection list of the statusbar. + +```ini +[env:Custom_UM] +extends = env:esp32c3dev +custom_usermods = UM_PluginDemo UM_DummySensor +``` + +This example is for an **ESP32-C3**. Replace `env:esp32c3dev` with one of these common controllers: +- `env:esp32dev` = The good old ESP32 +- `env:esp8266_2m` = Generic ESP8266 +- `env:esp01_1m_full` = ESP-01 + +And don't forget to have a look into `platformio_override.sample.ini` and +`platformio.ini` for much more inspiration! + + +## Ideas for potential future APIs + +- `BatterySensor` with `uint16_t batteryLevel()`, with range 0 ... 1000 representing 0.0 ... 100.0% + (or just `float` as for the other APIs) +- `TimeProvider` with `getTime()`, returning a struct of `year/month/day` & `hour/minute/second` + - Local time; without DST and timezone. + - With usermod implementations, based on I2C or OneWire RTC or NTP or DCF77 or ... +- `AudioSensor` with ... + - ... be careful to stay generic with these interfaces! +- `UiClient_PowerButton` for a generic handling of all usermod's power-buttons in the info-page of the UI. + - Thus a lot of repeated boilerplate code from many usermods could be consolidated inside PluginManager. +- `UiClient_InfoSection` with a more comfortable API for adding entries to the info-page of the UI. + - Also enables consolidation of repeated boilerplate code from the usermods. +- `ButtonUser` with `void onButtonPressed()`, for plugins that need an external trigger from a pushbutton. + - WLED somehow integrates these into its existing button management, as it already does for macros + in the UI configuration under "Button actions". + - So the plugin doesn't have to deal with raw GPIO pins when it just needs a simple trigger. +- MQTT publisher / subscriber +- audioreactive (since this can practically be considered as an essential part of WLED) + - `under construction` +- ... ? diff --git a/wled00/PluginAPI/TemperatureSensor.h b/wled00/PluginAPI/TemperatureSensor.h new file mode 100644 index 0000000000..59742a99cb --- /dev/null +++ b/wled00/PluginAPI/TemperatureSensor.h @@ -0,0 +1,46 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#pragma once + +//-------------------------------------------------------------------------------------------------- + +/// Interface of a sensor that can provide temperature readings. +class TemperatureSensor +{ +public: + /// Returns \c false when the sensor is not ready. + bool isReady() { return _isTemperatureValid; } + + /// Get the temperature, with the unit according to the global setting of \a useFahrenheit() + float temperature() { return useFahrenheit() ? temperatureF() : temperatureC(); } + + /// Get the temperature in °C + float temperatureC() { return do_getTemperatureC(); } + + /// Get the temperature in °F + float temperatureF() { return c2f(do_getTemperatureC()); } + + /** Determines the unit for \c temperature() - default is °C + * @see PluginManager::setUseFahrenheit() + */ + static bool useFahrenheit() { return _useFahrenheit; } + + /// Small helper to convert °C into °F + static float c2f(float degC) { return degC * 9.0f / 5.0f + 32.0f; } + +protected: + /// Get the plugin's temperature reading in °C + virtual float do_getTemperatureC() = 0; + + /// The plugin must set this to \c true after the sensor has been initialized. + bool _isTemperatureValid = false; + +private: + friend class PluginManager; + static bool _useFahrenheit; +}; + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/custom/DummySensor/DummySensor.cpp b/wled00/PluginAPI/custom/DummySensor/DummySensor.cpp new file mode 100644 index 0000000000..0ea3e88a3d --- /dev/null +++ b/wled00/PluginAPI/custom/DummySensor/DummySensor.cpp @@ -0,0 +1,7 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#include "DummySensor.h" +CPPFILE_PLUGIN_API(DummySensor); diff --git a/wled00/PluginAPI/custom/DummySensor/DummySensor.h b/wled00/PluginAPI/custom/DummySensor/DummySensor.h new file mode 100644 index 0000000000..de7cb043b9 --- /dev/null +++ b/wled00/PluginAPI/custom/DummySensor/DummySensor.h @@ -0,0 +1,30 @@ +/** + * (c) 2026 Joachim Dick + * Licensed under the EUPL v. 1.2 or later + */ + +#pragma once + +#include "PluginAPI/PluginMacros.h" + +//-------------------------------------------------------------------------------------------------- + +/** This is a plugin API that is very specific only for the DummySensor usermod. + * Other usermods or effects can directly interact with the usermod through this API. + * They can determine at runtime if the usermod is included in the WLED binary or not. + */ +class DummySensor +{ +public: + virtual void enableTemperatureSensor() = 0; + virtual void disableTemperatureSensor() = 0; + virtual bool isTemperatureSensorEnabled() = 0; + + virtual void enableHumiditySensor() = 0; + virtual void disableHumiditySensor() = 0; + virtual bool isHumiditySensorEnabled() = 0; +}; + +DECLARE_PLUGIN_API(DummySensor); + +//-------------------------------------------------------------------------------------------------- diff --git a/wled00/PluginAPI/custom/README.md b/wled00/PluginAPI/custom/README.md new file mode 100644 index 0000000000..c6c5a2af76 --- /dev/null +++ b/wled00/PluginAPI/custom/README.md @@ -0,0 +1,4 @@ +# Home for specific plugin APIs of WLED's usermods. + +This directory here **_is_** the right place for specific custom interfaces of usermods.
+Create a subdirectory for your usermod, and place your API files inside there. diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index a488d24f70..6b6efc8fdb 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -41,6 +41,7 @@ enum struct PinOwner : uint8_t { HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32) DMX_INPUT = 0x8D, // 'DMX_INPUT' == DMX input via serial HUB75 = 0x8E, // 'Hub75' == Hub75 driver + PluginMgr = 0xFE, // PluginManager // Use UserMod IDs from const.h here UM_Unspecified = USERMOD_ID_UNSPECIFIED, // 0x01 UM_Example = USERMOD_ID_EXAMPLE, // 0x02 // Usermod "usermod_v2_example.h" diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 647757ad6f..c68c719b0d 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -40,7 +40,8 @@ bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { } void UsermodManager::addToJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToJsonState(obj); } void UsermodManager::addToJsonInfo(JsonObject& obj) { - auto um_id_list = obj.createNestedArray("um"); + auto um_id_list = obj.createNestedArray("um"); + pluginManager.addToJsonInfo(obj); for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { um_id_list.add((*mod)->getId()); (*mod)->addToJsonInfo(obj); diff --git a/wled00/wled.h b/wled00/wled.h index 66b33740d6..be1237e06c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -195,6 +195,7 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #include "bus_manager.h" #include "FX.h" #include "wled_metadata.h" +#include "PluginAPI/PluginManager.h" #ifndef CLIENT_SSID #define CLIENT_SSID DEFAULT_CLIENT_SSID