Skip to content

Conversation

@qistoph
Copy link

@qistoph qistoph commented Oct 22, 2025

This is a combined PR for some improvements on the PWM Fan usermod.

More precise RPM calculation
Use milliseconds between actual measurements to calculate the desired speed instead of the configured seconds between checks.

Show locked state when tacho is used
The user interface shows Speed: locked/auto when the tacho pin is not used. This is useful information that is lost when the tacho pin is enabled, by showing Speed: 1500rpm. This PR changes it to:

  • Speed: 1500 rpm (locked)
  • Speed: 1500 rpm (auto)

Linear PWM control
Before this PW the PWM speed is configured and calculated using

  • Desired temperature (default: 35°C)
  • Number of PWM steps (default: 7)
  • Temperature difference per PWM step (default: 0.5°C)
desmos-graph

This PR changes the calculation to allow linear PWM control, instead of steps, and includes defaults to match the previous outcomes.

  • Desired temperature (default: 34.5°C)
  • Temperature at which fan should be at maximum speed (default: 38"C)
desmos-graph(1)

Disable lock from UI with speed 0
There was no way to disable the fan lock from the user interface. It would stay on until a reset or JSON API request. A user without access to the JSON API would not be able to turn to automatic fan control from the interface.

This PR changes the client side javascript call to unlock the automatic control when the user selects speed 0.

Side effect: the user can no longer disable the fan using the manual override, but would have to change the fan settings.

If any of these changes are undesired or require further improvement, please let me know. I'd be happy to help and look forward to have these changes included in the beautiful WLED.

Summary by CodeRabbit

  • New Features

    • Configurable maximum temperature ceiling to control fan max speed.
    • UI/state shows max-temperature control and explicit auto vs. locked RPM display.
  • Bug Fixes

    • More accurate RPM timing for tachometer readings.
    • Temperature-to-PWM mapping reworked to smooth linear scaling with bounds/validation and steadier fan response.
  • Chores

    • Persisted max-temperature setting across restarts; pins reinitialize when changed and state updates reflect changes immediately.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

Walkthrough

Changed PWM fan logic to use a configurable maxSpeedTemperature (34.5 default) and linear PWM interpolation between pwmMinValue and pwmMaxValue; removed step-based PWM logic. updateTacho() now takes an elapsed-ms parameter; tacho/RPM display and config persistence (including _temperatureMax) were updated and pins reinitialized when changed.

Changes

Cohort / File(s) Change Summary
PWM Fan Core
usermods/PWM_fan/PWM_fan.cpp
Added maxSpeedTemperature (default 34.5) and _temperatureMax[] string; removed legacy PWM step constants/step-size logic; implemented direct linear PWM scaling between pwmMinValue and pwmMaxValue based on temperature delta with bounds checks and fail-safes for invalid config.
Tachometer & Loop Integration
usermods/PWM_fan/PWM_fan.cpp
Changed updateTacho() signature to updateTacho(unsigned long msInterval); callers updated to pass elapsed ms and store msLastTachoMeasurement; RPM calculation now uses msInterval.
Config / Persistence / UI
usermods/PWM_fan/PWM_fan.cpp
Persisted maxSpeedTemperature in addToConfig / readFromConfig; added _temperatureMax to public string table; exposed max temperature and updated RPM/state JSON/text to show auto vs locked when tacho available; reinitialize pins when pin-related config changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "Improvements to the PWM Fan usermod" is related to the changeset in that it accurately identifies the modified component, but it uses overly generic language that fails to convey the specific nature of the changes. The word "Improvements" is vague and non-descriptive, providing no meaningful information about what was actually changed. The PR involves several significant modifications, including a shift from stepped to linear PWM control, enhanced RPM calculations using millisecond intervals, UI enhancements for showing lock state, and configuration parameter changes; however, a teammate scanning commit history would not understand these key changes from the title alone. While the title is not misleading or unrelated, it does not clearly or specifically summarize the main objectives of the PR. Consider revising the title to be more specific about the primary changes. Examples might include "Replace stepped PWM control with linear interpolation in PWM Fan" or "Refactor PWM Fan with linear control and improved RPM calculation." This would help readers quickly understand the core improvements without needing to read the full PR description.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44f96de and 834556d.

📒 Files selected for processing (1)
  • usermods/PWM_fan/PWM_fan.cpp (8 hunks)
🔇 Additional comments (5)
usermods/PWM_fan/PWM_fan.cpp (5)

57-58: LGTM! Default temperature parameters updated for linear PWM control.

The new defaults align with the PR objectives: targetTemperature set to 34.5°C and maxSpeedTemperature at 38°C provide the bounds for the linear interpolation replacing the previous stepped approach.


169-191: LGTM! Linear PWM interpolation correctly implemented with failsafes.

The implementation includes:

  • Correct order of operations for percentage calculations (multiply before divide)
  • Failsafe for invalid min/max configuration (line 172-174)
  • Runtime division-by-zero guard (line 185-188)
  • Clean three-branch logic for below/above/within temperature range

218-229: LGTM! Precise elapsed time tracking implemented correctly.

The loop now:

  • Calculates actual elapsed milliseconds since the last measurement
  • Passes this value to updateTacho() for accurate RPM calculation
  • Updates the timestamp after the measurement

This addresses the PR objective of more precise RPM calculation using actual elapsed time instead of the configured interval.


264-271: LGTM! Lock state now visible in UI.

When a tachometer is present, the speed display now includes the control mode ("locked" for manual override, "auto" for temperature-based control), addressing the PR objective of making the lock state visible to users.


402-413: LGTM! String constant correctly defined.

The new _temperatureMax string constant follows the established pattern of PROGMEM strings to reduce flash memory usage and is properly integrated with the configuration system.


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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
usermods/PWM_fan/PWM_fan.cpp (1)

99-112: Improve RPM calculation precision.

The current approach uses integer division msInterval / 1000, which truncates fractional seconds and reduces precision. For example, with msInterval = 30500ms, the division yields 30, discarding 500ms. This contradicts the PR objective of "more precise RPM calculation."

Apply this diff to eliminate intermediate truncation:

-    void updateTacho(unsigned long msInterval) {
-      if (tachoPin < 0) return;
-
-      // start of tacho measurement
-      // detach interrupt while calculating rpm
-      detachInterrupt(digitalPinToInterrupt(tachoPin)); 
-      // calculate rpm
-      last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation;
-      last_rpm /= msInterval / 1000;
-      // reset counter
-      counter_rpm = 0; 
-      // attach interrupt again
-      attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);
-    }
+    void updateTacho(unsigned long msInterval) {
+      if (tachoPin < 0) return;
+
+      // start of tacho measurement
+      // detach interrupt while calculating rpm
+      detachInterrupt(digitalPinToInterrupt(tachoPin)); 
+      // calculate rpm
+      last_rpm = (counter_rpm * 60 * 1000) / (numberOfInterrupsInOneSingleRotation * msInterval);
+      // reset counter
+      counter_rpm = 0; 
+      // attach interrupt again
+      attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);
+    }
🧹 Nitpick comments (1)
usermods/PWM_fan/PWM_fan.cpp (1)

183-183: Add explicit cast for clarity.

The expression speedFactor * pwmRange + pwmMinValue produces a float, which is implicitly converted to uint8_t when passed to updateFanSpeed. While legal, an explicit cast improves clarity.

Apply this diff:

-        updateFanSpeed(speedFactor * pwmRange + pwmMinValue);
+        updateFanSpeed((uint8_t)(speedFactor * pwmRange + pwmMinValue));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fff61b and 44fab44.

📒 Files selected for processing (1)
  • usermods/PWM_fan/PWM_fan.cpp (8 hunks)
🔇 Additional comments (5)
usermods/PWM_fan/PWM_fan.cpp (5)

217-217: Correctly passes actual elapsed interval.

The call now uses the actual milliseconds elapsed since the last measurement, aligning with the PR objective of more precise RPM calculation.


219-221: LGTM!

Storing the measurement timestamp enables accurate interval calculation in the next iteration, supporting the improved RPM precision.


251-251: LGTM!

The slider now correctly unlocks automatic control when speed is set to 0 ('lock':parseInt(this.value)>0), implementing the PR objective.


259-260: Improved UI feedback.

Now displays the lock/auto state alongside RPM values, implementing the PR objective of showing state when tacho is used.


319-319: Configuration persistence implemented correctly.

The new maxSpeedTemperature parameter is properly persisted and restored, consistent with other configuration parameters.

Also applies to: 353-353, 400-400

Copy link
Collaborator

@blazoncek blazoncek left a comment

Choose a reason for hiding this comment

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

As an author and maintainer of the usermod I see no real added value in the proposed changes.
There is no benefit if fan is running at 33.333% of speed compared to 7-step 28.5% or 42.8%.
There are several other issues I could spot.

if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return;

updateTacho();
updateTacho(now - msLastTachoMeasurement);
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's bound to be roughly 30s due to the statement above.

Copy link
Author

Choose a reason for hiding this comment

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

That assumes that the call is effectively called every 30s. It could throw off the result if this is not the case due to high load.

It's a little improvement to make the code more robust.

@qistoph
Copy link
Author

qistoph commented Oct 23, 2025

As an author and maintainer of the usermod I see no real added value in the proposed changes. There is no benefit if fan is running at 33.333% of speed compared to 7-step 28.5% or 42.8%. There are several other issues I could spot.

Changing the speed more granularly makes it less noticeable when it happens and becoming less of a disturbance.

What's your suggestion / request on the PR? Will it be accepted after fixing the requested changes? Do you need more explanation or not see it being accepted anyway?

@blazoncek
Copy link
Collaborator

becoming less of a disturbance.

Apparently you must be using this usermod with different intention than the one used when developing it. The idea is to provide ventilation to a controller enclosure, not living room ventilation.

Adding code to already crowded WLED which yields little real benefit is not really desired. However this is a (not default) usermod and some wiggle room may be exerted. I am willing to accept linear (or non linear for that matter) speed adjustment, but only that. No need to change polling interval beyond the ability for user to select one.

Perhaps @srg74 would also like to be informed.

Integer division evaluates before multiplication, causing minPWMValuePct / 100 to be 0 for all values less than 100. For example, with minPWMValuePct = 50:
- Current: 50 / 100 * 255 = 0 * 255 = 0 ❌
- Correct: (50 * 255) / 100 = 127 ✓
When pwmMaxValue <= pwmMinValue (due to misconfiguration), the function returns without updating the fan speed, leaving it at its previous state. This could cause overheating if the fan isn't already running at an appropriate speed.
@qistoph
Copy link
Author

qistoph commented Oct 23, 2025

The fan I'm using is to used to ventilate the enclosure. The enclosure and LEDs are however in my living area.

I understand and support preventing WLED to become some overcrowded multi-tool. The changes I'm providing reduce the complexity of calculations, reduce the number of parameters used, makes the RPM calculation more reliable and robust and adds a three features.

If some of these are undesirable, could you please be more specific in the desirable changes I'm suggesting?

  1. More precise and reliable RPM calculation
  2. Show locked state when tacho is used (feature 1)
  3. Linear PWM control
    a. Set temperature for fan max speed, instead of 7*0.5=3.5°C above target temperature. (feature 2)
  4. Disable lock from UI with speed 0 (feature 3)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
usermods/PWM_fan/PWM_fan.cpp (1)

57-58: Add validation to ensure maxSpeedTemperature > targetTemperature.

Despite the previous review comment, no validation has been added to prevent maxSpeedTemperature <= targetTemperature. This will cause division by zero at line 185 when both temperatures are equal, or inverted fan behavior when max is less than target.

The claim that "this is checked before the critical division" appears incorrect—line 182's check temp > maxSpeedTemperature only validates the measured temperature, not the relationship between the two configuration parameters.

Add validation in readFromConfig after line 356:

 maxSpeedTemperature = top[FPSTR(_temperatureMax)] | maxSpeedTemperature;
+if (maxSpeedTemperature <= targetTemperature) {
+  maxSpeedTemperature = targetTemperature + 0.5f; // ensure at least 0.5°C difference
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44fab44 and 8c28def.

📒 Files selected for processing (1)
  • usermods/PWM_fan/PWM_fan.cpp (8 hunks)
🔇 Additional comments (3)
usermods/PWM_fan/PWM_fan.cpp (3)

99-99: Good improvement for RPM accuracy.

Passing the actual elapsed milliseconds makes the RPM calculation independent of the tachoUpdateSec polling interval and more robust under variable system load.


262-263: Good UX improvement.

Displaying the lock/auto state alongside RPM provides clear feedback about the fan's current control mode.


223-224: ****

The timestamp reset at line 224 (msLastTachoMeasurement = now;) was already present in the previous version (HEAD~1). This is not a new addition or bug fix introduced in the current changes. The git history confirms the line existed before the latest commit.

Likely an incorrect or invalid review comment.

**Prevent potential division by zero in RPM calculation.**

The expression msInterval / 1000 uses integer division. If msInterval < 1000 (less than 1 second), the result is 0, causing division by zero when dividing last_rpm.

While the loop check at line 218 ensures at least tachoUpdateSec * 1000 ms have elapsed, defensive programming should handle edge cases.

**Use absolute maximum PWM for failsafe, not configured maximum.**

When pwmMaxValue <= pwmMinValue due to misconfiguration, the failsafe sets the fan to pwmMaxValue, which is the lower of the two values. This could result in insufficient cooling.

For a true failsafe, use _pwmMaxValue (255) to ensure maximum cooling.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
usermods/PWM_fan/PWM_fan.cpp (1)

339-386: Validate temperature relationship in config loading.

The readFromConfig method loads both targetTemperature and maxSpeedTemperature but doesn't validate that maxSpeedTemperature > targetTemperature. This allows invalid configurations to be persisted and loaded, leading to division by zero during fan speed calculation.

Add validation after line 356:

 maxSpeedTemperature = top[FPSTR(_temperatureMax)] | maxSpeedTemperature;
+// ensure maxSpeedTemperature exceeds targetTemperature
+if (maxSpeedTemperature <= targetTemperature) {
+  DEBUG_PRINT(F("Invalid config: max-temp-C ("));
+  DEBUG_PRINT(maxSpeedTemperature);
+  DEBUG_PRINT(F(") must exceed target-temp-C ("));
+  DEBUG_PRINT(targetTemperature);
+  DEBUG_PRINTLN(F("). Using defaults."));
+  targetTemperature = 34.5f;
+  maxSpeedTemperature = 38.0f;
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c28def and 44f96de.

📒 Files selected for processing (1)
  • usermods/PWM_fan/PWM_fan.cpp (8 hunks)
🔇 Additional comments (6)
usermods/PWM_fan/PWM_fan.cpp (6)

57-58: LGTM!

The default values for the new linear PWM control parameters are reasonable and ensure maxSpeedTemperature > targetTemperature at initialization.

Also applies to: 73-73


99-112: Add defensive check for msInterval == 0.

While the loop logic (line 218) ensures at least tachoUpdateSec * 1000 ms have elapsed, defensive programming should explicitly guard against edge cases.

Apply this diff to add a defensive check:

 void updateTacho(unsigned long msInterval) {
   if (tachoPin < 0) return;
+  if (msInterval == 0) return; // defensive: avoid division by zero
 
   // start of tacho measurement
   // detach interrupt while calculating rpm
   detachInterrupt(digitalPinToInterrupt(tachoPin)); 
   // calculate rpm
   last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation;
   last_rpm = (last_rpm * 1000) / msInterval;
   // reset counter
   counter_rpm = 0; 
   // attach interrupt again
   attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);
 }

169-175: LGTM!

The integer division order has been corrected, and the failsafe properly uses _pwmMaxValue for absolute maximum speed when configuration is invalid. These fixes address the critical issues identified in previous reviews.


214-225: LGTM!

Passing the actual elapsed milliseconds (now - msLastTachoMeasurement) to updateTacho makes the RPM calculation more precise and robust, especially under high system load when the loop timing may vary slightly from the configured interval.


259-267: LGTM!

The UI enhancement clearly indicates whether the fan is in locked or auto mode, improving transparency for users when a tachometer is present.


290-297: Ongoing discussion: Lock/unlock UI behavior.

The current implementation at line 293 automatically locks the fan when pwmValuePct > 0. Based on the past review discussion, the maintainer (@blazoncek) expressed preference for a separate UI button approach (option 2) rather than changing backend logic, stating: "I'm willing to take compromise in the form of separate UI button for Lock/Unlock functionality (2). But no other changes to UI or backend functionality."

The current implementation doesn't align with that stated preference. Please clarify with the maintainer whether this automatic-locking approach is acceptable or if you should implement the separate button approach instead.

Comment on lines 180 to 187
if (temp < targetTemperature) {
updateFanSpeed(pwmMinValue);
} else if(temp > maxSpeedTemperature) {
updateFanSpeed(pwmMaxValue);
} else {
float speedFactor = (temp - targetTemperature) / (maxSpeedTemperature - targetTemperature); // 0 - 1
updateFanSpeed(speedFactor * pwmRange + pwmMinValue);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation to prevent division by zero.

The linear interpolation at line 185 computes (temp - targetTemperature) / (maxSpeedTemperature - targetTemperature). If maxSpeedTemperature == targetTemperature, this results in division by zero.

Your past comment stated "This is checked before the critical division," but no such check is present in the code. The else clause is reached when targetTemperature <= temp <= maxSpeedTemperature, but there is no validation preventing maxSpeedTemperature from equaling targetTemperature.

Apply this diff to add a defensive check before the interpolation:

   } else if(temp > maxSpeedTemperature) {
     updateFanSpeed(pwmMaxValue);
   } else {
+    if (maxSpeedTemperature <= targetTemperature) {
+      updateFanSpeed(pwmMaxValue); // failsafe: invalid config
+      return;
+    }
     float speedFactor = (temp - targetTemperature) / (maxSpeedTemperature - targetTemperature); // 0 - 1
     updateFanSpeed(speedFactor * pwmRange + pwmMinValue);
   }

Additionally, consider adding validation in readFromConfig (after line 356) to log a warning and clamp the value:

if (maxSpeedTemperature <= targetTemperature) {
  DEBUG_PRINTLN(F("WARNING: max-temp-C must exceed target-temp-C. Adjusting."));
  maxSpeedTemperature = targetTemperature + 0.5f;
}
🤖 Prompt for AI Agents
In usermods/PWM_fan/PWM_fan.cpp around lines 180-187 the else-branch performs a
division by (maxSpeedTemperature - targetTemperature) without guarding against
zero; add a defensive check so that if maxSpeedTemperature equals
targetTemperature you skip the interpolation and set speedFactor to 0 (or
directly set the appropriate fan speed), avoiding division by zero. Also, in
readFromConfig after line 356 add validation that if maxSpeedTemperature <=
targetTemperature you log a warning (using DEBUG_PRINTLN with the provided
message) and clamp maxSpeedTemperature to targetTemperature + 0.5f to ensure the
range is valid.

Add validation to prevent division by zero.

The linear interpolation at line 185 computes (temp - targetTemperature) / (maxSpeedTemperature - targetTemperature). If maxSpeedTemperature == targetTemperature, this results in division by zero.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants