Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions usermods/DHT/DHT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -168,6 +172,7 @@ class UsermodDHT : public Usermod {

if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) {
disabled = true;
isSensorReady = false;
}
}

Expand Down Expand Up @@ -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);
REGISTER_USERMOD(dht);
14 changes: 12 additions & 2 deletions usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "wled.h"

class InternalTemperatureUsermod : public Usermod
class InternalTemperatureUsermod : public Usermod, public Sensor
{

private:
Expand All @@ -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()
{
}
Expand Down Expand Up @@ -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";
Expand All @@ -194,4 +204,4 @@ void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain)
}

static InternalTemperatureUsermod internal_temperature_v2;
REGISTER_USERMOD(internal_temperature_v2);
REGISTER_USERMOD(internal_temperature_v2);
8 changes: 6 additions & 2 deletions usermods/Temperature/Temperature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -379,4 +383,4 @@ static void mode_temperature() {


static UsermodTemperature temperature;
REGISTER_USERMOD(temperature);
REGISTER_USERMOD(temperature);
5 changes: 5 additions & 0 deletions usermods/Temperature/UsermodTemperature.h
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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; }
};

5 changes: 5 additions & 0 deletions usermods/UM_SensorDummy/README.md
Original file line number Diff line number Diff line change
@@ -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.
105 changes: 105 additions & 0 deletions usermods/UM_SensorDummy/UM_SensorDummy.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines 30 to 37
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The getSensor index handling is inverted and potentially buggy.

The current logic returns:

  • _sensorArray (Weather Station) when index == 0
  • this (SEF sensor) when index != 0 (including 1, 2, 3, etc.)

This means any index ≥ 2 also returns the SEF sensor, which is inconsistent with getSensorCount() returning 2. Consider explicit bounds checking:

🐛 Proposed fix
  Sensor *getSensor(uint8_t index) override
  {
-   if (index != 0)
-     return this;
-   return &_sensorArray;
+   if (index == 0)
+     return &_sensorArray;
+   if (index == 1)
+     return this;
+   return nullptr;
  }
🤖 Prompt for AI Agents
In `@usermods/UM_SensorDummy/UM_SensorDummy.cpp` around lines 30 - 37, The
getSensor implementation has inverted logic and lacks bounds checking: change
getSensor to first validate index (< getSensorCount()) and return nullptr for
out-of-range indices, then map index 0 to the SEF sensor (this) and index 1 to
the weather sensor (_sensorArray); update the method (getSensor) to use explicit
checks or a switch so indices >=2 cannot incorrectly return this.

Copy link
Author

Choose a reason for hiding this comment

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

@coderabbitai Added comments that invalid indices result in undefined behaviour (just like out of bound array access) -- thus anything can happen. In those cases, I prefer "the UB" to be a valid (although unexpected) sensor over a nullptr to avoid crashes.
So unexperienced developers can recognize that something is not working as expected, which is more helpful and less frustrating than crashes. And it avoids range checks that are not really beneficial.
Aside from that, do we have adequate assert macros in WLED? Those would be appropriate here, assuming that debug builds are a relevant thing at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for assert macros and debug assertions in WLED codebase
echo "=== Searching for assert definitions and usage ==="
rg -n --type cpp --type c -C2 '#define.*assert' | head -50

echo -e "\n=== Searching for assert usage patterns ==="
rg -n --type cpp --type c -C1 '\bassert\s*\(' | head -30

echo -e "\n=== Searching for DEBUG or debug build patterns ==="
rg -n --type cpp --type c -C2 '#ifdef.*DEBUG|#if.*DEBUG' | head -40

echo -e "\n=== Checking for WLED-specific assertion patterns ==="
rg -n --type cpp --type c 'WLED_ASSERT|DEBUG_PRINT.*assert|USER_PRINT.*assert' | head -20

Length of output: 2844


🏁 Script executed:

#!/bin/bash
# Check for standard assert.h includes and usage patterns
echo "=== Checking for assert.h includes ==="
rg -n --type cpp --type c '#include.*assert\.h' 

echo -e "\n=== Looking at the assert usage in um_manager.cpp ==="
cat -n wled00/um_manager.cpp | sed -n '90,105p'

echo -e "\n=== Checking for DEBUG_PRINT and other debug macros ==="
rg -n --type cpp --type c '#define.*DEBUG_PRINT' | head -20

echo -e "\n=== Checking const.h for debug-related defines ==="
fd -e h const.h --exec cat {} | head -100

Length of output: 6503


@JoaDick, I completely understand your rationale—returning a valid sensor provides more graceful degradation than nullptr crashes, especially helpful for learning developers who can see "wrong behavior" rather than hitting a segfault.

Based on my search of the codebase, here's what's available for assertions:

Standard C assert(): Available and already used in WLED. For example, in wled00/um_manager.cpp:96:

assert(!oappend_shim);

Debug builds: WLED supports debug builds via the WLED_DEBUG flag, which enables:

  • DEBUG_PRINT(x), DEBUG_PRINTLN(x), DEBUG_PRINTF(x...) macros (defined in wled00/wled.h)
  • These compile to no-ops when WLED_DEBUG is not defined
  • Additional specialized flags like WLED_DEBUG_MATH for specific subsystems

No WLED-specific assert macro currently exists. If you want consistent assertion behavior across the codebase, you could consider adding something like:

`#ifdef` WLED_DEBUG
  `#define` WLED_ASSERT(condition) assert(condition)
`#else`
  `#define` WLED_ASSERT(condition) ((void)0)
`#endif`

However, given your preference for graceful degradation and the minimal use of assertions in the current codebase, documenting the UB behavior in comments (as you've done) seems like a pragmatic approach that aligns well with WLED's design philosophy.


✏️ Learnings added
Learnt from: JoaDick
Repo: wled/WLED PR: 5344
File: usermods/UM_SensorDummy/UM_SensorDummy.cpp:30-37
Timestamp: 2026-02-14T22:28:29.696Z
Learning: In WLED sensor implementations (like UM_SensorDummy.cpp), when getSensor() receives out-of-bounds indices, the preferred behavior is to return a valid (though unexpected) sensor rather than nullptr. This prevents crashes and helps unexperienced developers recognize incorrect behavior rather than encountering frustrating segfaults. Bounds checking can be skipped in favor of graceful degradation, with UB documented in comments.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: mval-sg
Repo: wled/WLED PR: 4876
File: wled00/xml.cpp:0-0
Timestamp: 2025-08-28T08:09:20.630Z
Learning: The WLED codebase has opportunities for refactoring hardcoded array bounds (like the "15" used for DMX channels) to use sizeof(array)/sizeof(array[0]) for more maintainable code, but such changes should be done consistently across the entire codebase in a dedicated refactoring effort.

Learnt from: willmmiles
Repo: wled/WLED PR: 4939
File: wled00/FX_fcn.cpp:1183-1194
Timestamp: 2025-09-18T03:17:30.107Z
Learning: In WLED bus management code, dead code branches in error handling (like unreachable else breaks) are intentionally preserved to accommodate potential future API changes, even when currently unreachable.

Learnt from: JoaDick
Repo: wled/WLED PR: 5319
File: usermods/UM_PluginDemo/UM_PluginDemo.cpp:62-86
Timestamp: 2026-01-26T00:04:16.470Z
Learning: In WLED (wled00/FX_fcn.cpp), Segment::setPixelColor() performs internal bounds checking, returning early if the index is negative or exceeds vLength(). No additional guards are needed when calling this function, even with potentially out-of-range indices.

Learnt from: blazoncek
Repo: wled/WLED PR: 4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs by falling back to the main segment ID, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5251
File: usermods/user_fx/user_fx.cpp:146-148
Timestamp: 2026-01-01T07:19:40.244Z
Learning: In the WLED Ants effect (usermods/user_fx/user_fx.cpp), the color alternation logic `antIndex % 3 == 1` is intentional and creates a 1/3 vs 2/3 distribution between SEGCOLOR(0) and SEGCOLOR(2). This unbalanced alternation is by design and should not be changed to a balanced mod 2 alternation.

Learnt from: blazoncek
Repo: wled/WLED PR: 4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5109
File: wled00/FX.cpp:3174-3343
Timestamp: 2025-11-27T06:33:11.436Z
Learning: WLED Ants effect (wled00/FX.cpp): The author prefers the current velocity initialization using hw_random16(1000, 5000)/5000.0f, resulting in an effective range of ~3.6–10.0 (with VELOCITY_MIN=2.0, VELOCITY_MAX=10.0), and wants the code kept as-is with comments updated to document this behavior. Avoid suggesting changes to span the full 2.0–10.0 range in future reviews.

Learnt from: blazoncek
Repo: wled/WLED PR: 4658
File: wled00/led.cpp:90-91
Timestamp: 2025-04-24T09:31:06.879Z
Learning: In the WLED codebase, the `getSegment()` function guards against out-of-bounds segment IDs, and `getFirstSelectedSegId()` falls back to `getMainSegmentId()` if no segments are selected, ensuring no crashes when used through the `setValuesFromFirstSelectedSeg()` macro.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:11.994Z
Learning: In WLED code reviews, when code is modified or added, look for "magic numbers" (hardcoded numeric literals) and suggest replacing them with appropriate defined constants when those constants are meaningful in the context of the PR. For example, the hardcoded value 32 should be replaced with WLED_MAX_SEGNAME_LEN when it represents a segment name length limit. This improves code maintainability and reduces the risk of inconsistencies.

Learnt from: DedeHai
Repo: wled/WLED PR: 4997
File: wled00/e131.cpp:33-44
Timestamp: 2025-10-10T18:34:06.550Z
Learning: In WLED's DDP packet handling (ws.cpp and e131.cpp), only prevent out-of-bounds memory access rather than enforcing DDP spec compliance. Don't check the 1440-byte spec limit—accept out-of-spec packets assuming correct encoding. The bounds check `maxDataIndex = c + numLeds * ddpChannelsPerLed; if (maxDataIndex > dataLen) reject` is sufficient and already covers the timecode flag case (when c=4) without needing separate validation.

Learnt from: DedeHai
Repo: wled/WLED PR: 4939
File: wled00/data/index.js:2406-2413
Timestamp: 2025-09-28T09:53:42.670Z
Learning: In WLED, code paths that appear unreachable may actually be safety fallbacks for low-heap conditions. These fallbacks are intentional defensive programming measures to ensure UI functionality is preserved when system resources are constrained.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:22:20.525Z
Learning: In WLED's image_loader.cpp and similar contexts, when suggesting cleanup or de-allocation code in error handlers, first verify whether the underlying library or function already handles cleanup after failure. For example, `new` expressions that throw exceptions do not assign partial pointers and automatically clean up storage, making explicit dealloc() calls in catch blocks unnecessary. Only suggest "safety" cleanup when the library/function's documented semantics do not already provide graceful failure handling.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-11-14T13:37:30.955Z
Learning: In WLED code reviews, when code is modified or added, look for "magic numbers" (hardcoded numeric literals) and suggest replacing them with defined constants when meaningful constants exist in the codebase. For example, suggest replacing hardcoded "32" with WLED_MAX_SEGNAME_LEN if the context relates to segment name length limits.


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);
4 changes: 4 additions & 0 deletions usermods/UM_SensorDummy/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "UM_SensorDummy",
"build": { "libArchive": false }
}
5 changes: 5 additions & 0 deletions usermods/UM_SensorInfo/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading