diff --git a/usermods/DHT/DHT.cpp b/usermods/DHT/DHT.cpp
index 2ed3dd0ace..47be99b035 100644
--- a/usermods/DHT/DHT.cpp
+++ b/usermods/DHT/DHT.cpp
@@ -57,13 +57,15 @@
DHT_nonblocking dht_sensor(DHTPIN, DHTTYPE);
-class UsermodDHT : public Usermod {
+class UsermodDHT : public Usermod, public Sensor {
private:
+ static const char _name[];
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;
+ bool isSensorReady = false;
#ifdef USERMOD_DHT_MQTT
char dhtMqttTopic[64];
size_t dhtMqttTopicLen;
@@ -79,6 +81,8 @@ class UsermodDHT : public Usermod {
#endif
public:
+ UsermodDHT() : Sensor{_name, 2} {}
+
void setup() {
nextReadTime = millis() + USERMOD_DHT_FIRST_MEASUREMENT_AT;
lastReadTime = millis();
@@ -112,13 +116,13 @@ 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
+ isSensorReady = 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 +172,7 @@ class UsermodDHT : public Usermod {
if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) {
disabled = true;
+ isSensorReady = false;
}
}
@@ -242,8 +247,18 @@ class UsermodDHT : public Usermod {
return USERMOD_ID_DHT;
}
+ uint8_t getSensorCount() override { return 1; }
+ Sensor *getSensor(uint8_t) override { return this; }
+
+ private:
+ bool do_isSensorReady() override { return isSensorReady; }
+ SensorValue do_getSensorChannelValue(uint8_t index) override { return index == 0 ? humidity : tempC; }
+ const SensorChannelProps &do_getSensorChannelProperties(uint8_t index) override { return _channelProps[index]; }
+ const SensorChannelPropsArray<2> _channelProps = { makeChannelProps_Humidity(), makeChannelProps_Temperature() };
};
+const char UsermodDHT::_name[] PROGMEM = "DHT";
+
static UsermodDHT dht;
-REGISTER_USERMOD(dht);
\ No newline at end of file
+REGISTER_USERMOD(dht);
diff --git a/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp b/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp
index 7c30985eea..d90e73e743 100644
--- a/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp
+++ b/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp
@@ -1,6 +1,6 @@
#include "wled.h"
-class InternalTemperatureUsermod : public Usermod
+class InternalTemperatureUsermod : public Usermod, public Sensor
{
private:
@@ -22,10 +22,17 @@ class InternalTemperatureUsermod : public Usermod
static const char _activationThreshold[];
static const char _presetToActivate[];
+ const SensorChannelProps sensorProps = makeChannelProps_Temperature("on-chip", 0.0f, 80.0f);
+ bool do_isSensorReady() override { return isEnabled && temperature >= 0.0f; }
+ SensorValue do_getSensorChannelValue(uint8_t) override { return temperature; }
+ const SensorChannelProps &do_getSensorChannelProperties(uint8_t) override { return sensorProps; }
+
// any private methods should go here (non-inline method should be defined out of class)
void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message
public:
+ InternalTemperatureUsermod() : Sensor{"CPU", 1} {}
+
void setup()
{
}
@@ -171,6 +178,9 @@ class InternalTemperatureUsermod : public Usermod
{
return USERMOD_ID_INTERNAL_TEMPERATURE;
}
+
+ uint8_t getSensorCount() override { return 1; }
+ Sensor *getSensor(uint8_t) override { return this; }
};
const char InternalTemperatureUsermod::_name[] PROGMEM = "Internal Temperature";
@@ -194,4 +204,4 @@ void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain)
}
static InternalTemperatureUsermod internal_temperature_v2;
-REGISTER_USERMOD(internal_temperature_v2);
\ No newline at end of file
+REGISTER_USERMOD(internal_temperature_v2);
diff --git a/usermods/Temperature/Temperature.cpp b/usermods/Temperature/Temperature.cpp
index 6dcaa70c6b..6bcdeac3fa 100644
--- a/usermods/Temperature/Temperature.cpp
+++ b/usermods/Temperature/Temperature.cpp
@@ -165,11 +165,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;
+ temperatureSensor.suspendSensor();
+ }
lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms
return;
}
errorCount = 0;
+ temperatureSensor = temperature;
#ifndef WLED_DISABLE_MQTT
if (WLED_MQTT_CONNECTED) {
@@ -379,4 +383,4 @@ static void 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..1e92a3b3ca 100644
--- a/usermods/Temperature/UsermodTemperature.h
+++ b/usermods/Temperature/UsermodTemperature.h
@@ -50,6 +50,8 @@ class UsermodTemperature : public Usermod {
int16_t idx = -1; // Domoticz virtual sensor idx
uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3
+ EasySensor temperatureSensor{_name, makeChannelProps_Temperature()};
+
// strings to reduce flash memory usage (used more than twice)
static const char _name[];
static const char _enabled[];
@@ -106,5 +108,8 @@ class UsermodTemperature : public Usermod {
bool readFromConfig(JsonObject &root) override;
void appendConfigData() override;
+
+ uint8_t getSensorCount() override { return 1; }
+ Sensor *getSensor(uint8_t) override { return &temperatureSensor; }
};
diff --git a/usermods/UM_SensorDummy/README.md b/usermods/UM_SensorDummy/README.md
new file mode 100644
index 0000000000..8c61c5dd94
--- /dev/null
+++ b/usermods/UM_SensorDummy/README.md
@@ -0,0 +1,5 @@
+# Dummy usermod to simulate random sensor readings.
+
+Use `UM_SensorInfo` and `UM_SensorDummy` together as an example for how sensors are implemented,
+and how its data can be retrieved. The generated sensor data can be processed by effects and other
+usermods - without directly knowing the sensor and its specific type of data.
diff --git a/usermods/UM_SensorDummy/UM_SensorDummy.cpp b/usermods/UM_SensorDummy/UM_SensorDummy.cpp
new file mode 100644
index 0000000000..21b2ccdf76
--- /dev/null
+++ b/usermods/UM_SensorDummy/UM_SensorDummy.cpp
@@ -0,0 +1,105 @@
+/**
+ * (c) 2026 Joachim Dick
+ * Licensed under the EUPL v. 1.2 or later
+ */
+
+#include "wled.h"
+
+//--------------------------------------------------------------------------------------------------
+
+/** Dummy usermod implementation that simulates random sensor readings.
+ */
+class UM_SensorDummy : public Usermod, public Sensor
+{
+public:
+ UM_SensorDummy() : Sensor{"SEF", 4} {}
+
+ // ----- usermod functions -----
+
+ void setup() override {}
+
+ void loop() override
+ {
+ const auto now = millis();
+ if (now < _nextUpdateTime)
+ return;
+ readWeatherStation();
+ _nextUpdateTime = now + 20; // 50 sensor updates per second
+ }
+
+ uint8_t getSensorCount() override { return 2; }
+
+ Sensor *getSensor(uint8_t index) override
+ {
+ if (index == 0)
+ return &_sensorArray;
+ return this;
+ }
+
+ bool do_isSensorReady() override { return true; }
+ SensorValue do_getSensorChannelValue(uint8_t channelIndex) override { return readSEF(channelIndex); }
+ const SensorChannelProps &do_getSensorChannelProperties(uint8_t channelIndex) override { return _localSensorProps[channelIndex]; }
+
+ // ----- internal processing functions -----
+
+ void readWeatherStation()
+ {
+ _sensorArray.set(0, 1012.34f);
+ _sensorArray.set(1, readTemperature());
+ _sensorArray.set(2, readHumidity());
+#if (0) // Battery is empty :-(
+ _sensorArray.set(3, readTemperature() - 3.0f);
+ _sensorArray.set(4, readHumidity() - 5.0f);
+#endif
+ }
+
+ /// 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;
+ }
+
+ float readSEF(uint8_t index)
+ {
+ if (index >= 3)
+ {
+ const int32_t raw = abs(beat16(20) - 0x8000);
+ return raw / 32767.0f * 100.0f;
+ }
+ const int32_t raw = beatsin16_t(40, 0, 0xFFFF, 0, (index * 0xFFFF) / 3);
+ return 90.0f + raw / 65535.0f * 90.0f;
+ }
+
+ // ----- member variables -----
+
+ uint32_t _nextUpdateTime = 0;
+
+ const SensorChannelPropsArray<4> _localSensorProps =
+ {{makeChannelProps_Float("deltaX", {"offset", "°rad"}, 0.0f, 360.0f),
+ makeChannelProps_Float("deltaY", {"offset", "°rad"}, 0.0f, 360.0f),
+ makeChannelProps_Float("deltaZ", {"offset", "°rad"}, 0.0f, 360.0f),
+ makeChannelProps_Float("deltaT", {"jitter", "µs"}, -1000.0f, 1000.0f)}};
+
+ EasySensorArray<5> _sensorArray{"Weather Station",
+ {{SensorChannelProps{"Barometer",
+ SensorQuantity::AirPressure(), 950.0f, 1050.0f},
+ makeChannelProps_Temperature("Indoor Temp."),
+ makeChannelProps_Humidity("Indoor Hum."),
+ makeChannelProps_Temperature("Outdoor Temp."),
+ makeChannelProps_Humidity("Outdoor Hum.")}}};
+};
+
+//--------------------------------------------------------------------------------------------------
+
+static UM_SensorDummy um_SensorDummy;
+REGISTER_USERMOD(um_SensorDummy);
diff --git a/usermods/UM_SensorDummy/library.json b/usermods/UM_SensorDummy/library.json
new file mode 100644
index 0000000000..ae97d90ecf
--- /dev/null
+++ b/usermods/UM_SensorDummy/library.json
@@ -0,0 +1,4 @@
+{
+ "name": "UM_SensorDummy",
+ "build": { "libArchive": false }
+}
diff --git a/usermods/UM_SensorInfo/README.md b/usermods/UM_SensorInfo/README.md
new file mode 100644
index 0000000000..db57c1d1f1
--- /dev/null
+++ b/usermods/UM_SensorInfo/README.md
@@ -0,0 +1,5 @@
+# Examples for working with sensors.
+
+Use `UM_SensorInfo` and `UM_SensorDummy` together as an example for how sensors are implemented,
+and how its data can be retrieved. The generated sensor data can be processed by effects and other
+usermods - without directly knowing the sensor and its specific type of data.
diff --git a/usermods/UM_SensorInfo/UM_SensorInfo.cpp b/usermods/UM_SensorInfo/UM_SensorInfo.cpp
new file mode 100644
index 0000000000..7634998663
--- /dev/null
+++ b/usermods/UM_SensorInfo/UM_SensorInfo.cpp
@@ -0,0 +1,270 @@
+/**
+ * (c) 2026 Joachim Dick
+ * Licensed under the EUPL v. 1.2 or later
+ */
+
+#include "wled.h"
+
+extern void mode_static(void);
+
+//--------------------------------------------------------------------------------------------------
+
+namespace
+{
+
+ void drawLine(int pos, int length, uint32_t color)
+ {
+ const int end = pos + length;
+ while (pos < end)
+ SEGMENT.setPixelColor(pos++, color);
+ }
+
+ class SensorChannelVisualizer
+ {
+ public:
+ const uint8_t offset = 3;
+ const uint8_t size = 16;
+ const uint8_t space = 3;
+
+ void showInfo(Sensor &sensor)
+ {
+ _startPos = offset + _sensorCounter * (size + space);
+
+ showReadyState(sensor);
+ showChannelInfo(sensor);
+
+ ++_sensorCounter;
+ }
+
+ private:
+ void showReadyState(Sensor &sensor)
+ {
+ drawLine(_startPos, size, sensor.isSensorReady() ? 0x002200 : 0x220000);
+ }
+
+ void showChannelInfo(Sensor &sensor)
+ {
+ int pos = _startPos;
+ const uint8_t channelCount = sensor.channelCount();
+ for (uint8_t ch = 0; ch < MIN(channelCount, size / 2); ++ch)
+ {
+ SensorChannelProxy channel = sensor.getChannel(ch);
+ const uint32_t color = getChannelColor(channel);
+ SEGMENT.setPixelColor(pos, color);
+ pos += 2;
+ }
+ }
+
+ static uint32_t getChannelColor(SensorChannelProxy &channel)
+ {
+ return channel.isReady() ? 0x008844 : 0x880044;
+ }
+
+ private:
+ uint8_t _sensorCounter = 0;
+ int _startPos;
+ };
+
+}
+
+//--------------------------------------------------------------------------------------------------
+
+void mode_SensorInfo()
+{
+ SEGMENT.clear();
+
+ SensorChannelVisualizer visualizer;
+
+ for (auto cursor = UsermodManager::getSensors(); cursor.isValid(); cursor.next())
+ {
+ visualizer.showInfo(cursor.get());
+ }
+}
+static const char _data_FX_MODE_SENSOR_INFO[] PROGMEM = "1 Sensor Info";
+
+//--------------------------------------------------------------------------------------------------
+
+void mode_NumberDumper()
+{
+ SEGMENT.clear();
+
+ uint8_t hue = 0;
+ for (SensorChannelsByType cursor{UsermodManager::getSensors(), SensorValueType::Float}; cursor.isValid(); cursor.next())
+ {
+ auto channel = cursor.get();
+ const bool isReady = channel.isReady();
+ if (!isReady)
+ continue;
+
+ const auto &props = channel.getProps();
+ const float sensorValue{channel.getValue()};
+ const float sensorValueMax{props.rangeMax};
+ // TODO(feature) Also take care for negative ranges, and map accordingly.
+ if (sensorValueMax > 0.0f)
+ {
+ const int pos = sensorValue * (SEGLEN - 1) / sensorValueMax;
+ uint32_t color;
+ hsv2rgb(CHSV32(hue, 255, 255), color);
+ SEGMENT.setPixelColor(pos, color);
+ hue += 74;
+ }
+ }
+}
+static const char _data_FX_MODE_NUMBER_DUMPER[] PROGMEM = "2 Numbers";
+
+//--------------------------------------------------------------------------------------------------
+
+void mode_SEF_all()
+{
+ Sensor *sensor = findSensorByName(UsermodManager::getSensors(), "SEF");
+ if (!sensor || !sensor->isSensorReady())
+ {
+ mode_static();
+ return;
+ }
+
+ SEGMENT.clear();
+ SEGMENT.fill(0x080800);
+
+ uint8_t hue = 0;
+ for (uint8_t channelIndex = 0; channelIndex < sensor->channelCount(); ++channelIndex)
+ {
+ if (!sensor->isChannelReady(channelIndex))
+ continue;
+
+ const auto &props = sensor->getChannelProps(channelIndex);
+ const float sensorValue{sensor->getChannelValue(channelIndex)};
+ const float sensorValueMax{props.rangeMax};
+ // TODO(feature) Also take care for negative ranges, and map accordingly.
+ if (sensorValueMax > 0.0f)
+ {
+ const int pos = sensorValue * (SEGLEN - 1) / sensorValueMax;
+ uint32_t color;
+ hsv2rgb(CHSV32(hue, 255, 255), color);
+ SEGMENT.setPixelColor(pos, color);
+ hue += 74;
+ }
+ }
+}
+static const char _data_FX_MODE_SEF_ALL[] PROGMEM = "3 SEF all";
+
+//--------------------------------------------------------------------------------------------------
+
+class FluctuationChannels final : public SensorChannelCursor
+{
+public:
+ explicit FluctuationChannels(SensorCursor allSensors)
+ : SensorChannelCursor{allSensors} {}
+
+private:
+ bool matches(const SensorChannelProps &props) override { return strcmp(props.quantity.name, "offset") == 0; }
+};
+
+void mode_Fluctuations()
+{
+ SEGMENT.clear();
+ SEGMENT.fill(0x080800);
+
+ uint8_t hue = 0;
+ for (FluctuationChannels cursor{UsermodManager::getSensors()}; cursor.isValid(); cursor.next())
+ {
+ auto channel = cursor.get();
+ if (!channel.isReady())
+ continue;
+
+ const auto &props = channel.getProps();
+ const float sensorValue{channel.getValue()};
+ const float sensorValueMax{props.rangeMax};
+ // TODO(feature) Also take care for negative ranges, and map accordingly.
+ if (sensorValueMax > 0.0f)
+ {
+ const int pos = sensorValue * (SEGLEN - 1) / sensorValueMax;
+ uint32_t color;
+ hsv2rgb(CHSV32(hue, 255, 255), color);
+ SEGMENT.setPixelColor(pos, color);
+ hue += 74;
+ }
+ }
+}
+static const char _data_FX_MODE_FLUCTUATIONS[] PROGMEM = "4 Fluct only";
+
+//--------------------------------------------------------------------------------------------------
+
+class UM_SensorInfo : public Usermod
+{
+ void setup() override
+ {
+ strip.addEffect(255, &mode_SensorInfo, _data_FX_MODE_SENSOR_INFO);
+ strip.addEffect(255, &mode_NumberDumper, _data_FX_MODE_NUMBER_DUMPER);
+ strip.addEffect(255, &mode_SEF_all, _data_FX_MODE_SEF_ALL);
+ strip.addEffect(255, &mode_Fluctuations, _data_FX_MODE_FLUCTUATIONS);
+ }
+
+ void loop() override {}
+
+ void addToJsonInfo(JsonObject &root)
+ {
+ JsonObject user = root["u"];
+ if (user.isNull())
+ user = root.createNestedObject("u");
+
+ int sensorIndex = 0;
+ for (auto cursor = UsermodManager::getSensors(); cursor.isValid(); cursor.next())
+ {
+ Sensor &sensor = cursor.get();
+ const bool isSensorReady = sensor.isSensorReady();
+ const int channelCount = sensor.channelCount();
+
+ String sensorName;
+ sensorName += sensorIndex;
+ sensorName += "._ ";
+ sensorName += sensor.name();
+ String sensorChannels;
+ if (!isSensorReady)
+ sensorChannels += "[OFFLINE] - ";
+ sensorChannels += channelCount;
+ sensorChannels += " channel";
+ if (channelCount > 1)
+ sensorChannels += 's';
+ user.createNestedArray(sensorName).add(sensorChannels);
+
+ for (int channelIndex = 0; channelIndex < channelCount; ++channelIndex)
+ {
+ SensorChannelProxy channel = sensor.getChannel(channelIndex);
+ const SensorChannelProps &channelProps = channel.getProps();
+ String key;
+ key += sensorIndex;
+ key += ".";
+ key += channelIndex;
+ key += " ";
+ key += channel.name();
+
+ const bool isChannelReady = channel.isReady();
+ String val;
+ val += channelProps.quantity.name;
+ val += "
";
+ if (isChannelReady)
+ {
+ const SensorValue sensorValue = channel.getValue();
+ // TODO(feature) Also take care for other datatypes (via visitor).
+ val += sensorValue.as_float();
+ }
+ else
+ {
+ val += "[n/a]";
+ }
+ val += " ";
+ val += channelProps.quantity.unit;
+
+ user.createNestedArray(key).add(val);
+ }
+
+ ++sensorIndex;
+ }
+ }
+};
+
+//--------------------------------------------------------------------------------------------------
+
+static UM_SensorInfo um_SensorInfo;
+REGISTER_USERMOD(um_SensorInfo);
diff --git a/usermods/UM_SensorInfo/library.json b/usermods/UM_SensorInfo/library.json
new file mode 100644
index 0000000000..8d54854459
--- /dev/null
+++ b/usermods/UM_SensorInfo/library.json
@@ -0,0 +1,4 @@
+{
+ "name": "UM_SensorInfo",
+ "build": { "libArchive": false }
+}
diff --git a/wled00/Sensor.cpp b/wled00/Sensor.cpp
new file mode 100644
index 0000000000..522817dcd3
--- /dev/null
+++ b/wled00/Sensor.cpp
@@ -0,0 +1,164 @@
+/**
+ * (c) 2026 Joachim Dick
+ * Licensed under the EUPL v. 1.2 or later
+ */
+
+#include "wled.h"
+#include "Sensor.h"
+
+//--------------------------------------------------------------------------------------------------
+
+void SensorCursor::reset()
+{
+ _sensorIndex = 0;
+ for (_umIter = _umBegin; _umIter < _umEnd; ++_umIter)
+ {
+ Usermod *um = *_umIter;
+ if (um->getSensorCount())
+ {
+ _sensor = um->getSensor(0);
+ if (_sensor && _sensor->channelCount())
+ return;
+ }
+ }
+ _sensor = nullptr;
+}
+
+bool SensorCursor::next()
+{
+ if (isValid())
+ {
+ // try next sensor of current usermod
+ Usermod *um = *_umIter;
+ if (++_sensorIndex < um->getSensorCount())
+ {
+ _sensor = um->getSensor(_sensorIndex);
+ if (_sensor && _sensor->channelCount())
+ return true;
+ }
+ // try next usermod
+ _sensorIndex = 0;
+ while (++_umIter < _umEnd)
+ {
+ um = *_umIter;
+ if (um->getSensorCount())
+ {
+ _sensor = um->getSensor(0);
+ if (_sensor && _sensor->channelCount())
+ return true;
+ }
+ }
+ // no more sensors found
+ _sensor = nullptr;
+ }
+ return false;
+}
+
+Sensor *findSensorByName(SensorCursor allSensors, const char *sensorName)
+{
+ while (allSensors.isValid())
+ {
+ if (strcmp(allSensors->name(), sensorName) == 0)
+ return &allSensors.get();
+ allSensors.next();
+ }
+ return nullptr;
+}
+
+//--------------------------------------------------------------------------------------------------
+
+bool SensorChannelCursor::isValid()
+{
+ if (!_sensorCursor.isValid())
+ return false;
+ if (matches(_sensorCursor->getChannelProps(_channelIndex)))
+ return true;
+ return next();
+}
+
+void SensorChannelCursor::reset()
+{
+ _sensorCursor.reset();
+ _channelIndex = 0;
+}
+
+bool SensorChannelCursor::next()
+{
+ while (_sensorCursor.isValid())
+ {
+ if (++_channelIndex < _sensorCursor->channelCount())
+ if (matches(_sensorCursor->getChannelProps(_channelIndex)))
+ return true;
+
+ _channelIndex = 0;
+ if (_sensorCursor.next())
+ if (matches(_sensorCursor->getChannelProps(_channelIndex)))
+ return true;
+ }
+ return false;
+}
+
+//--------------------------------------------------------------------------------------------------
+
+void SensorValue::accept(SensorValueVisitor &visitor) const
+{
+ switch (_type)
+ {
+ case SensorValueType::Bool:
+ visitor.visit(_bool);
+ break;
+ case SensorValueType::Float:
+ visitor.visit(_float);
+ break;
+ case SensorValueType::Int32:
+ visitor.visit(_int32);
+ break;
+ case SensorValueType::UInt32:
+ visitor.visit(_uint32);
+ break;
+ case SensorValueType::Array:
+ visitor.visit(*_array);
+ break;
+ case SensorValueType::Struct:
+ visitor.visit(*_struct);
+ break;
+ case SensorValueType::Whatever:
+ visitor.visit(_whatever);
+ break;
+ }
+}
+
+void Sensor::accept(uint8_t channelIndex, SensorChannelVisitor &visitor)
+{
+ if (!isSensorReady())
+ return;
+
+ const auto &val = getChannelValue(channelIndex);
+ const auto &props = getChannelProps(channelIndex);
+ switch (val.type())
+ {
+ case SensorValueType::Bool:
+ visitor.visit(val._bool, props);
+ break;
+ case SensorValueType::Float:
+ visitor.visit(val._float, props);
+ break;
+ case SensorValueType::Int32:
+ visitor.visit(val._int32, props);
+ break;
+ case SensorValueType::UInt32:
+ visitor.visit(val._uint32, props);
+ break;
+ case SensorValueType::Array:
+ visitor.visit(*val._array, props);
+ break;
+ case SensorValueType::Struct:
+ visitor.visit(*val._struct, props);
+ break;
+ case SensorValueType::Whatever:
+ visitor.visit(val._whatever, props);
+ break;
+ }
+}
+
+//--------------------------------------------------------------------------------------------------
diff --git a/wled00/Sensor.h b/wled00/Sensor.h
new file mode 100644
index 0000000000..e4ddef31ab
--- /dev/null
+++ b/wled00/Sensor.h
@@ -0,0 +1,524 @@
+/**
+ * (c) 2026 Joachim Dick
+ * Licensed under the EUPL v. 1.2 or later
+ */
+
+#pragma once
+
+#include
+
+//--------------------------------------------------------------------------------------------------
+
+class SensorValueArray; // TODO(feature) Not implemented yet!
+class SensorValueStruct; // TODO(feature) Not implemented yet!
+
+class SensorValueVisitor;
+class SensorValueArrayVisitor; // TODO(feature) Not implemented yet!
+class SensorValueStructVisitor; // TODO(feature) Not implemented yet!
+class SensorChannelVisitor;
+
+/// Specific datatype that is contained in a SensorValue.
+enum class SensorValueType : uint8_t
+{
+ invalid = 0, //< SensorValue is empty.
+ Bool,
+ Float,
+ Int32,
+ UInt32,
+ Array,
+ Struct,
+ Whatever
+};
+
+/// Generic datatype that is delivered by a SensorChannel.
+class SensorValue
+{
+public:
+ /// Default constructor creates an invalid SensorValue.
+ SensorValue() : _type{SensorValueType::invalid} {}
+
+ SensorValue(bool val) : _bool{val}, _type{SensorValueType::Bool} {}
+ SensorValue(float val) : _float{val}, _type{SensorValueType::Float} {}
+ SensorValue(int32_t val) : _int32{val}, _type{SensorValueType::Int32} {}
+ SensorValue(uint32_t val) : _uint32{val}, _type{SensorValueType::UInt32} {}
+ explicit SensorValue(const SensorValueArray *val) : _array{val}, _type{SensorValueType::Array} {}
+ explicit SensorValue(const SensorValueStruct *val) : _struct{val}, _type{SensorValueType::Struct} {}
+ explicit SensorValue(const void *val) : _whatever{val}, _type{SensorValueType::Whatever} {}
+
+ /** Check if the SensorValue is valid.
+ * Converting an invalid SensorValue to a specific type results in undefined behaviour.
+ */
+ bool isValid() const { return _type != SensorValueType::invalid; }
+
+ SensorValueType type() const { return _type; }
+
+ void accept(SensorValueVisitor &visitor) const;
+
+ bool as_bool() const { return _type == SensorValueType::Bool ? _bool : false; }
+ float as_float() const { return _type == SensorValueType::Float ? _float : 0.0f; }
+ int32_t as_int32() const { return _type == SensorValueType::Int32 ? _int32 : 0; }
+ uint32_t as_uint32() const { return _type == SensorValueType::UInt32 ? _uint32 : 0U; }
+ const SensorValueArray *as_array() const { return _type == SensorValueType::Array ? _array : nullptr; }
+ const SensorValueStruct *as_struct() const { return _type == SensorValueType::Struct ? _struct : nullptr; }
+ const void *as_whatever() const { return _type == SensorValueType::Whatever ? _whatever : nullptr; }
+
+ explicit operator bool() const { return as_bool(); }
+ explicit operator float() const { return as_float(); }
+ explicit operator int32_t() const { return as_int32(); }
+ explicit operator uint32_t() const { return as_uint32(); }
+ explicit operator const SensorValueArray *() const { return as_array(); }
+ explicit operator const SensorValueStruct *() const { return as_struct(); }
+ explicit operator const void *() const { return as_whatever(); }
+
+private:
+ friend class Sensor;
+ union
+ {
+ bool _bool;
+ float _float;
+ int32_t _int32;
+ uint32_t _uint32;
+ const SensorValueArray *_array;
+ const SensorValueStruct *_struct;
+ const void *_whatever;
+ };
+
+ SensorValueType _type;
+};
+
+/// The physical (or theoretical/virtual) quantity of the readings from a sensor channel.
+struct SensorQuantity
+{
+ const char *const name;
+ const char *const unit;
+
+ // common physical measurements (just for convenience and consistency)
+ static SensorQuantity Temperature() { return {"Temperature", "°C"}; }
+ static SensorQuantity Humidity() { return {"Humidity", "%rel"}; }
+ static SensorQuantity AirPressure() { return {"Air Pressure", "hPa"}; }
+
+ static SensorQuantity Voltage() { return {"Voltage", "V"}; }
+ static SensorQuantity Current() { return {"Current", "A"}; }
+ static SensorQuantity Power() { return {"Power", "W"}; }
+ static SensorQuantity Energy() { return {"Energy", "kWh"}; }
+
+ static SensorQuantity Angle() { return {"Angle", "°"}; }
+
+ static SensorQuantity Percent() { return {"Percent", "%"}; }
+};
+
+/// Properties of a SensorChannel.
+struct SensorChannelProps
+{
+ SensorChannelProps(const char *channelName_,
+ SensorQuantity quantity_,
+ SensorValue rangeMin_,
+ SensorValue rangeMax_)
+ : channelName{channelName_}, quantity{quantity_}, rangeMin{rangeMin_}, rangeMax{rangeMax_} {}
+
+ const char *const channelName; //< The channel's name.
+ const SensorQuantity quantity; //< The quantity of the channel's readings.
+ const SensorValue rangeMin; //< The readings' (typical) minimum range of operation.
+ const SensorValue rangeMax; //< The readings' (typical) maximum range of operation.
+};
+
+/// Helper array for multiple SensorChannelProps.
+template
+using SensorChannelPropsArray = std::array;
+
+//--------------------------------------------------------------------------------------------------
+class SensorChannelProxy;
+
+/// Interface to be implemented by all sensors.
+class Sensor
+{
+public:
+ /// Get the sensor's name.
+ const char *name() { return _sensorName; }
+
+ /// Get the number of provided sensor channels.
+ uint8_t channelCount() { return _channelCount; }
+
+ /// Check if the sensor is online and ready to be used.
+ bool isSensorReady() { return do_isSensorReady(); }
+
+ /** Get a proxy object that's representing one specific sensor channel.
+ * channelIndex >= channelCount() results in undefined behaviour.
+ */
+ SensorChannelProxy getChannel(uint8_t channelIndex);
+
+ /** Check if a specific sensor channel is ready to deliver data.
+ * channelIndex >= channelCount() results in undefined behaviour.
+ */
+ bool isChannelReady(uint8_t channelIndex) { return do_isSensorReady() ? do_isSensorChannelReady(channelIndex) : false; }
+
+ /** Read a value from a specific sensor channel.
+ * channelIndex >= channelCount() results in undefined behaviour.
+ * Reading a value while isChannelReady() returns false results in undefined behaviour.
+ */
+ SensorValue getChannelValue(uint8_t channelIndex) { return do_getSensorChannelValue(channelIndex); }
+
+ /** Get the properties of a specific sensor channel.
+ * channelIndex >= channelCount() results in undefined behaviour.
+ */
+ const SensorChannelProps &getChannelProps(uint8_t channelIndex) { return do_getSensorChannelProperties(channelIndex); }
+
+ /** Accept the given \a visitor for a specific sensor channel.
+ * channelIndex >= channelCount() results in undefined behaviour.
+ */
+ void accept(uint8_t channelIndex, SensorChannelVisitor &visitor);
+
+protected:
+ Sensor(const char *sensorName, uint8_t channelCount)
+ : _sensorName{sensorName}, _channelCount{channelCount} {}
+
+ virtual bool do_isSensorReady() = 0;
+ virtual bool do_isSensorChannelReady(uint8_t channelIndex) { return true; };
+ virtual SensorValue do_getSensorChannelValue(uint8_t channelIndex) = 0;
+ virtual const SensorChannelProps &do_getSensorChannelProperties(uint8_t channelIndex) = 0;
+
+private:
+ const char *_sensorName;
+ const uint8_t _channelCount;
+};
+
+/// A proxy object that is representing one specific sensor channel.
+class SensorChannelProxy
+{
+public:
+ SensorChannelProxy(Sensor &parentSensor, const uint8_t channelIndex)
+ : _parent{parentSensor}, _index{channelIndex} {}
+
+ /// Get the channel's name.
+ const char *name() { return getProps().channelName; }
+
+ /// Check if the channel is ready to deliver data.
+ bool isReady() { return _parent.isChannelReady(_index); }
+
+ /** Read a value from the channel.
+ * Reading a value while isReady() returns false results in undefined behaviour.
+ */
+ SensorValue getValue() { return _parent.getChannelValue(_index); }
+
+ /// Get the channel's properties.
+ const SensorChannelProps &getProps() { return _parent.getChannelProps(_index); }
+
+ /// Get the channel's corresponding origin sensor.
+ Sensor &getRealSensor() { return _parent; }
+
+ /// Get the channel's corresponding index at the origin sensor.
+ uint8_t getRealChannelIndex() { return _index; }
+
+private:
+ Sensor &_parent;
+ const uint8_t _index;
+};
+
+inline SensorChannelProxy Sensor::getChannel(uint8_t channelIndex) { return SensorChannelProxy{*this, channelIndex}; }
+
+//--------------------------------------------------------------------------------------------------
+
+class SensorValueVisitor
+{
+public:
+ virtual void visit(bool val) {}
+ virtual void visit(float val) {}
+ virtual void visit(int32_t val) {}
+ virtual void visit(uint32_t val) {}
+ virtual void visit(const SensorValueArray &val) {}
+ virtual void visit(const SensorValueStruct &val) {}
+ virtual void visit(const void *val) {}
+};
+
+class SensorChannelVisitor
+{
+public:
+ virtual void visit(bool val, const SensorChannelProps &props) {}
+ virtual void visit(float val, const SensorChannelProps &props) {}
+ virtual void visit(int32_t val, const SensorChannelProps &props) {}
+ virtual void visit(uint32_t val, const SensorChannelProps &props) {}
+ virtual void visit(const SensorValueArray &val, const SensorChannelProps &props) {}
+ virtual void visit(const SensorValueStruct &val, const SensorChannelProps &props) {}
+ virtual void visit(const void *val, const SensorChannelProps &props) {}
+};
+
+//--------------------------------------------------------------------------------------------------
+class Usermod;
+
+/// A cursor to iterate over all available sensors (provided by UsermodManager).
+class SensorCursor
+{
+public:
+ using UmIterator = Usermod *const *;
+ SensorCursor(UmIterator umBegin, UmIterator umEnd) : _umBegin{umBegin}, _umEnd{umEnd} { reset(); }
+
+ /// Check if the cursor has currently selected a valid sensor instance.
+ bool isValid() const { return _sensor != nullptr; }
+
+ /** Get the currently selected sensor instance.
+ * Getting the sensor while isValid() returns false results in undefined behaviour.
+ */
+ Sensor &get() { return *_sensor; }
+ Sensor &operator*() { return *_sensor; }
+ Sensor *operator->() { return _sensor; }
+
+ /** Select the next sensor.
+ * @return Same as \c isValid()
+ */
+ bool next();
+
+ /// Jump back to the first sensor (if any).
+ void reset();
+
+private:
+ UmIterator _umBegin;
+ UmIterator _umEnd;
+ UmIterator _umIter = nullptr;
+ Sensor *_sensor = nullptr;
+ uint8_t _sensorIndex = 0;
+};
+
+Sensor *findSensorByName(SensorCursor allSensors, const char *sensorName);
+
+//--------------------------------------------------------------------------------------------------
+
+/// Base class for cursors that iterate over specific channels of all sensors.
+class SensorChannelCursor
+{
+public:
+ /// Check if the cursor has currently selected a valid sensor channel instance.
+ bool isValid();
+
+ /** Get the currently selected sensor channel instance.
+ * Getting the channel while isValid() returns false results in undefined behaviour.
+ */
+ SensorChannelProxy get() { return {*_sensorCursor, _channelIndex}; }
+
+ /** Select the next channel.
+ * @return Same as \c isValid()
+ */
+ bool next();
+
+ /// Jump back to the first channel (if any).
+ void reset();
+
+protected:
+ ~SensorChannelCursor() = default;
+ explicit SensorChannelCursor(SensorCursor allSensors) : _sensorCursor{allSensors} { reset(); }
+
+ virtual bool matches(const SensorChannelProps &channelProps) = 0;
+
+private:
+ SensorCursor _sensorCursor;
+ uint8_t _channelIndex = 0;
+};
+
+/// A cursor to iterate over all channels of all sensors.
+class AllSensorChannels final : public SensorChannelCursor
+{
+public:
+ explicit AllSensorChannels(SensorCursor allSensors)
+ : SensorChannelCursor{allSensors} {}
+
+private:
+ bool matches(const SensorChannelProps &) override { return true; }
+};
+
+/// A cursor to iterate over all channels with a specific quantity.
+class SensorChannelsByQuantity final : public SensorChannelCursor
+{
+public:
+ SensorChannelsByQuantity(SensorCursor allSensors, const char *quantityName)
+ : SensorChannelCursor{allSensors}, _quantityName{quantityName} {}
+
+ SensorChannelsByQuantity(SensorCursor allSensors, SensorQuantity quantity)
+ : SensorChannelsByQuantity{allSensors, quantity.name} {}
+
+private:
+ bool matches(const SensorChannelProps &props) override { return strcmp(props.quantity.name, _quantityName) == 0; }
+ const char *const _quantityName;
+};
+
+// A cursor to iterate over all channels with a specific ValueType.
+class SensorChannelsByType final : public SensorChannelCursor
+{
+public:
+ SensorChannelsByType(SensorCursor allSensors, SensorValueType valueType)
+ : SensorChannelCursor{allSensors}, _type{valueType} {}
+
+private:
+ bool matches(const SensorChannelProps &props) override { return props.rangeMin.type() == _type; }
+ const SensorValueType _type;
+};
+
+// A cursor to iterate over all channels with a specific name.
+class SensorChannelsByName final : public SensorChannelCursor
+{
+public:
+ SensorChannelsByName(SensorCursor allSensors, const char *channelName)
+ : SensorChannelCursor{allSensors}, _name{channelName} {}
+
+private:
+ bool matches(const SensorChannelProps &props) override { return strcmp(props.channelName, _name) == 0; }
+ const char *const _name;
+};
+
+//--------------------------------------------------------------------------------------------------
+
+/** Base class for simple sensor implementations.
+ * Supports at most 32 sensor channels.
+ * @see EasySensor
+ * @see EasySensorArray
+ */
+class EasySensorBase : public Sensor
+{
+public:
+ /// Put the entire sensor offline (by usermod).
+ void suspendSensor() { _isSensorReady = false; }
+
+ /** Put a specific sensor channel offline (by usermod).
+ * channelIndex >= channelCount() results in undefined behaviour.
+ */
+ void suspendChannel(uint8_t channelIndex) { _channelReadyFlags &= ~(1U << channelIndex); }
+
+ /** Store the readings for a specific sensor channel (by usermod).
+ * channelIndex >= channelCount() results in undefined behaviour.
+ */
+ void set(uint8_t channelIndex, SensorValue val)
+ {
+ if (channelIndex >= channelCount())
+ channelIndex = 0;
+ _channelValues[channelIndex] = val;
+ _channelReadyFlags |= (1U << channelIndex);
+ _isSensorReady = true;
+ }
+
+protected:
+ ~EasySensorBase() = default;
+ EasySensorBase(const char *sensorName, uint8_t channelCount,
+ SensorValue *channelValues, const SensorChannelProps *channelProps)
+ : Sensor{sensorName, channelCount},
+ _channelProps{channelProps}, _channelValues{channelValues} {}
+
+private:
+ bool do_isSensorReady() final { return _isSensorReady; }
+
+ bool do_isSensorChannelReady(uint8_t channelIndex) final
+ {
+ if (channelIndex >= channelCount())
+ channelIndex = 0;
+ return _channelReadyFlags & (1U << channelIndex);
+ };
+
+ SensorValue do_getSensorChannelValue(uint8_t channelIndex) final
+ {
+ if (channelIndex >= channelCount())
+ channelIndex = 0;
+ return _channelValues[channelIndex];
+ }
+
+ const SensorChannelProps &do_getSensorChannelProperties(uint8_t channelIndex) final
+ {
+ if (channelIndex >= channelCount())
+ channelIndex = 0;
+ return _channelProps[channelIndex];
+ }
+
+private:
+ const SensorChannelProps *_channelProps;
+ SensorValue *_channelValues;
+ uint32_t _channelReadyFlags = 0;
+ bool _isSensorReady = false;
+};
+
+/** A simple sensor implementation that provides multiple sensor channels.
+ * Most of the required sensor housekeeping is provided by this helper.
+ * The usermod is ultimately just responsible for:
+ * - Initialize this helper (as member variable) with the sensor's properties.
+ * - Make this helper available to the UsermodManager.
+ * - Periodically read the physical sensor.
+ * - Store the readings in this helper.
+ */
+template
+class EasySensorArray : public EasySensorBase
+{
+public:
+ static_assert(CHANNEL_COUNT <= 32, "EasySensorArray supports max. 32 sensor channels");
+
+ EasySensorArray(const char *sensorName, const SensorChannelPropsArray &channelProps)
+ : EasySensorBase{sensorName, CHANNEL_COUNT, _channelValues.data(), _channelProps.data()},
+ _channelProps{channelProps} {}
+
+private:
+ const SensorChannelPropsArray _channelProps;
+ std::array _channelValues;
+};
+
+/** A simple sensor implementation that provides one single sensor channel (at index 0).
+ * Most of the required sensor housekeeping is provided by this helper.
+ * The usermod is ultimately just responsible for:
+ * - Initialize this helper (as member variable) with the sensor's properties.
+ * - Make this helper available to the UsermodManager.
+ * - Periodically read the physical sensor.
+ * - Store the readings in this helper.
+ */
+class EasySensor : public EasySensorBase
+{
+public:
+ void suspendChannel(uint8_t channelIndex) = delete;
+ void set(uint8_t channelIndex, SensorValue val) = delete;
+
+ EasySensor(const char *sensorName, const SensorChannelProps &channelProps)
+ : EasySensorBase{sensorName, 1, &_val, &_props}, _props{channelProps} {}
+
+ /// Store the the readings for the sensor.
+ void set(SensorValue val) { EasySensorBase::set(0, val); }
+ void operator=(SensorValue val) { set(val); }
+
+private:
+ const SensorChannelProps _props;
+ SensorValue _val;
+};
+
+//--------------------------------------------------------------------------------------------------
+
+/// Create properties of a channel that delivers generic \c bool readings.
+inline SensorChannelProps makeChannelProps_Bool(const char *channelName,
+ const char *quantityName = nullptr)
+{
+ return {channelName, {quantityName ? quantityName : channelName, ""}, false, true};
+}
+
+/// Create properties of a channel that delivers generic \c float readings.
+inline SensorChannelProps makeChannelProps_Float(const char *channelName,
+ const SensorQuantity &channelQuantity,
+ float rangeMin,
+ float rangeMax)
+{
+ return {channelName, channelQuantity, rangeMin, rangeMax};
+}
+
+/// Create properties of a channel that delivers temperature readings.
+inline SensorChannelProps makeChannelProps_Temperature(const char *channelName,
+ float rangeMin = 0.0f,
+ float rangeMax = 40.0f)
+{
+ const auto quantity = SensorQuantity::Temperature();
+ return {channelName ? channelName : quantity.name, quantity, rangeMin, rangeMax};
+}
+
+/// Create properties of a channel that delivers temperature readings.
+inline SensorChannelProps makeChannelProps_Temperature(float rangeMin = 0.0f,
+ float rangeMax = 40.0f)
+{
+ return makeChannelProps_Temperature(nullptr, rangeMin, rangeMax);
+}
+
+/// Create properties of a channel that delivers humidity readings.
+inline SensorChannelProps makeChannelProps_Humidity(const char *channelName = nullptr)
+{
+ const auto quantity = SensorQuantity::Humidity();
+ return {channelName ? channelName : quantity.name, quantity, 0.0f, 100.0f};
+}
+
+//--------------------------------------------------------------------------------------------------
diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h
index 84b6da9e61..c00eb585a8 100644
--- a/wled00/fcn_declare.h
+++ b/wled00/fcn_declare.h
@@ -2,6 +2,8 @@
#ifndef WLED_FCN_DECLARE_H
#define WLED_FCN_DECLARE_H
+#include "Sensor.h"
+
/*
* All globally accessible functions are declared here
*/
@@ -336,6 +338,8 @@ class Usermod {
virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update
virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change
virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;}
+ virtual uint8_t getSensorCount() { return 0; } // get number of provided sensors
+ virtual Sensor *getSensor(uint8_t index) { return nullptr; } // get a specific sensor; index >= getSensorCount() results in undefined behaviour
// API shims
private:
@@ -377,6 +381,7 @@ namespace UsermodManager {
void onStateChange(uint8_t);
Usermod* lookup(uint16_t mod_id);
size_t getModCount();
+ SensorCursor getSensors();
};
// Register usermods by building a static list via a linker section
diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp
index 647757ad6f..ab128c08bf 100644
--- a/wled00/um_manager.cpp
+++ b/wled00/um_manager.cpp
@@ -98,3 +98,5 @@ void Usermod::appendConfigData(Print& settingsScript) {
this->appendConfigData();
oappend_shim = nullptr;
}
+
+SensorCursor UsermodManager::getSensors() { return SensorCursor{_usermod_table_begin, _usermod_table_end}; }