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}; }