Skip to content

Commit 84f4e3e

Browse files
Merge branch 'Aircoookie:master' into master
2 parents a28345d + b003ed3 commit 84f4e3e

File tree

4 files changed

+366
-2
lines changed

4 files changed

+366
-2
lines changed

usermods/PWM_fan/readme.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PWM fan
2+
3+
v2 Usermod to to control PWM fan with RPM feedback and temperature control
4+
5+
This usermod requires Dallas Temperature usermod to obtain temperature information. If this is not available the fan will always run at 100% speed.
6+
If the fan does not have _tacho_ (RPM) output you can set the _tacho-pin_ to -1 to not use that feature.
7+
8+
You can also set the thershold temperature at which fan runs at lowest speed. If the actual temperature measured will be 3°C greater than threshold temperature the fan will run at 100%.
9+
10+
If the _tacho_ is supported the current speed (in RPM) will be repored in WLED Info page.
11+
12+
## Installation
13+
14+
Add the compile-time option `-D USERMOD_PWM_FAN` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_PWM_FAN` in `myconfig.h`.
15+
You will also need `-D USERMOD_DALLASTEMPERATURE`.
16+
17+
### Define Your Options
18+
19+
All of the parameters are configured during run-time using Usermods settings page.
20+
This includes:
21+
22+
* PWM output pin
23+
* tacho input pin
24+
* sampling frequency in seconds
25+
* threshold temperature in degees C
26+
27+
_NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency.
28+
29+
### PlatformIO requirements
30+
31+
No special requirements.
32+
33+
## Change Log
34+
35+
2021-10
36+
* First public release

usermods/PWM_fan/usermod_PWM_fan.h

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
#pragma once
2+
3+
#ifndef USERMOD_DALLASTEMPERATURE
4+
#error The "PWM fan" usermod requires "Dallas Temeprature" usermod to function properly.
5+
#endif
6+
7+
#include "wled.h"
8+
9+
// PWM & tacho code curtesy of @KlausMu
10+
// https://github.com/KlausMu/esp32-fan-controller/tree/main/src
11+
// adapted for WLED usermod by @blazoncek
12+
13+
14+
// tacho counter
15+
static volatile unsigned long counter_rpm = 0;
16+
// Interrupt counting every rotation of the fan
17+
// https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/
18+
static void IRAM_ATTR rpm_fan() {
19+
counter_rpm++;
20+
}
21+
22+
23+
class PWMFanUsermod : public Usermod {
24+
25+
private:
26+
27+
bool initDone = false;
28+
bool enabled = true;
29+
30+
const int numberOfInterrupsInOneSingleRotation = 2; // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups.
31+
const int pwmMinimumValue = 120;
32+
const int pwmStep = 10;
33+
34+
unsigned long msLastTachoMeasurement = 0;
35+
uint16_t last_rpm = 0;
36+
#ifdef ARDUINO_ARCH_ESP32
37+
uint8_t pwmChannel = 255;
38+
#endif
39+
40+
#ifdef USERMOD_DALLASTEMPERATURE
41+
UsermodTemperature* tempUM;
42+
#endif
43+
44+
// configurable parameters
45+
int8_t tachoPin = -1;
46+
int8_t pwmPin = -1;
47+
uint8_t tachoUpdateSec = 30;
48+
float targetTemperature = 25.0;
49+
50+
// strings to reduce flash memory usage (used more than twice)
51+
static const char _name[];
52+
static const char _enabled[];
53+
static const char _tachoPin[];
54+
static const char _pwmPin[];
55+
static const char _temperature[];
56+
static const char _tachoUpdateSec[];
57+
58+
void initTacho(void) {
59+
if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){
60+
tachoPin = -1;
61+
return;
62+
}
63+
pinMode(tachoPin, INPUT);
64+
digitalWrite(tachoPin, HIGH);
65+
attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);
66+
DEBUG_PRINTLN(F("Tacho sucessfully initialized."));
67+
}
68+
69+
void deinitTacho(void) {
70+
if (tachoPin < 0) return;
71+
detachInterrupt(digitalPinToInterrupt(tachoPin));
72+
pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified);
73+
tachoPin = -1;
74+
}
75+
76+
void updateTacho(void) {
77+
if (tachoPin < 0) return;
78+
79+
// start of tacho measurement
80+
// detach interrupt while calculating rpm
81+
detachInterrupt(digitalPinToInterrupt(tachoPin));
82+
// calculate rpm
83+
last_rpm = counter_rpm * (60 / numberOfInterrupsInOneSingleRotation);
84+
last_rpm /= tachoUpdateSec;
85+
// reset counter
86+
counter_rpm = 0;
87+
// store milliseconds when tacho was measured the last time
88+
msLastTachoMeasurement = millis();
89+
// attach interrupt again
90+
attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);
91+
}
92+
93+
// https://randomnerdtutorials.com/esp32-pwm-arduino-ide/
94+
void initPWMfan(void) {
95+
if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) {
96+
pwmPin = -1;
97+
return;
98+
}
99+
100+
#ifdef ESP8266
101+
analogWriteRange(255);
102+
analogWriteFreq(WLED_PWM_FREQ);
103+
#else
104+
pwmChannel = pinManager.allocateLedc(1);
105+
if (pwmChannel == 255) { //no more free LEDC channels
106+
deinitPWMfan(); return;
107+
}
108+
// configure LED PWM functionalitites
109+
ledcSetup(pwmChannel, 25000, 8);
110+
// attach the channel to the GPIO to be controlled
111+
ledcAttachPin(pwmPin, pwmChannel);
112+
#endif
113+
DEBUG_PRINTLN(F("Fan PWM sucessfully initialized."));
114+
}
115+
116+
void deinitPWMfan(void) {
117+
if (pwmPin < 0) return;
118+
119+
pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified);
120+
#ifdef ARDUINO_ARCH_ESP32
121+
pinManager.deallocateLedc(pwmChannel, 1);
122+
#endif
123+
pwmPin = -1;
124+
}
125+
126+
void updateFanSpeed(uint8_t pwmValue){
127+
if (pwmPin < 0) return;
128+
129+
#ifdef ESP8266
130+
analogWrite(pwmPin, pwmValue);
131+
#else
132+
ledcWrite(pwmChannel, pwmValue);
133+
#endif
134+
}
135+
136+
float getActualTemperature(void) {
137+
#ifdef USERMOD_DALLASTEMPERATURE
138+
if (tempUM != nullptr)
139+
return tempUM->getTemperatureC();
140+
#endif
141+
return -127.0f;
142+
}
143+
144+
void setFanPWMbasedOnTemperature(void) {
145+
float temp = getActualTemperature();
146+
float difftemp = temp - targetTemperature;
147+
// Default to run fan at full speed.
148+
int newPWMvalue = 255;
149+
150+
if ((temp == NAN) || (temp <= 0.0)) {
151+
DEBUG_PRINTLN(F("WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255."));
152+
} else if (difftemp <= 0.0) {
153+
// Temperature is below target temperature. Run fan at minimum speed.
154+
newPWMvalue = pwmMinimumValue;
155+
} else if (difftemp <= 0.5) {
156+
newPWMvalue = 140;
157+
} else if (difftemp <= 1.0) {
158+
newPWMvalue = 160;
159+
} else if (difftemp <= 1.5) {
160+
newPWMvalue = 180;
161+
} else if (difftemp <= 2.0) {
162+
newPWMvalue = 200;
163+
} else if (difftemp <= 2.5) {
164+
newPWMvalue = 220;
165+
} else if (difftemp <= 3.0) {
166+
newPWMvalue = 240;
167+
}
168+
updateFanSpeed(newPWMvalue);
169+
}
170+
171+
public:
172+
173+
// gets called once at boot. Do all initialization that doesn't depend on
174+
// network here
175+
void setup() {
176+
#ifdef USERMOD_DALLASTEMPERATURE
177+
// This Usermod requires Temperature usermod
178+
tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE);
179+
#endif
180+
initTacho();
181+
initPWMfan();
182+
updateFanSpeed(pwmMinimumValue);
183+
initDone = true;
184+
}
185+
186+
// gets called every time WiFi is (re-)connected. Initialize own network
187+
// interfaces here
188+
void connected() {}
189+
190+
/*
191+
* Da loop.
192+
*/
193+
void loop() {
194+
if (!enabled || strip.isUpdating()) return;
195+
196+
unsigned long now = millis();
197+
if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return;
198+
199+
updateTacho();
200+
setFanPWMbasedOnTemperature();
201+
}
202+
203+
/*
204+
* addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.
205+
* Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI.
206+
* Below it is shown how this could be used for e.g. a light sensor
207+
*/
208+
void addToJsonInfo(JsonObject& root) {
209+
if (tachoPin < 0) return;
210+
JsonObject user = root["u"];
211+
if (user.isNull()) user = root.createNestedObject("u");
212+
JsonArray data = user.createNestedArray(FPSTR(_name));
213+
data.add(last_rpm);
214+
data.add(F("rpm"));
215+
}
216+
217+
/*
218+
* addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).
219+
* Values in the state object may be modified by connected clients
220+
*/
221+
//void addToJsonState(JsonObject& root) {
222+
//}
223+
224+
/*
225+
* readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).
226+
* Values in the state object may be modified by connected clients
227+
*/
228+
//void readFromJsonState(JsonObject& root) {
229+
// if (!initDone) return; // prevent crash on boot applyPreset()
230+
//}
231+
232+
/*
233+
* addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object.
234+
* It will be called by WLED when settings are actually saved (for example, LED settings are saved)
235+
* If you want to force saving the current state, use serializeConfig() in your loop().
236+
*
237+
* CAUTION: serializeConfig() will initiate a filesystem write operation.
238+
* It might cause the LEDs to stutter and will cause flash wear if called too often.
239+
* Use it sparingly and always in the loop, never in network callbacks!
240+
*
241+
* addToConfig() will also not yet add your setting to one of the settings pages automatically.
242+
* To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually.
243+
*
244+
* I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!
245+
*/
246+
void addToConfig(JsonObject& root) {
247+
JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
248+
top[FPSTR(_enabled)] = enabled;
249+
top[FPSTR(_pwmPin)] = pwmPin;
250+
top[FPSTR(_tachoPin)] = tachoPin;
251+
top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec;
252+
top[FPSTR(_temperature)] = targetTemperature;
253+
DEBUG_PRINTLN(F("Autosave config saved."));
254+
}
255+
256+
/*
257+
* readFromConfig() can be used to read back the custom settings you added with addToConfig().
258+
* This is called by WLED when settings are loaded (currently this only happens once immediately after boot)
259+
*
260+
* readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),
261+
* but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup.
262+
* If you don't know what that is, don't fret. It most likely doesn't affect your use case :)
263+
*
264+
* The function should return true if configuration was successfully loaded or false if there was no configuration.
265+
*/
266+
bool readFromConfig(JsonObject& root) {
267+
int8_t newTachoPin = tachoPin;
268+
int8_t newPwmPin = pwmPin;
269+
270+
JsonObject top = root[FPSTR(_name)];
271+
DEBUG_PRINT(FPSTR(_name));
272+
if (top.isNull()) {
273+
DEBUG_PRINTLN(F(": No config found. (Using defaults.)"));
274+
return false;
275+
}
276+
277+
enabled = top[FPSTR(_enabled)] | enabled;
278+
newTachoPin = top[FPSTR(_tachoPin)] | newTachoPin;
279+
newPwmPin = top[FPSTR(_pwmPin)] | newPwmPin;
280+
tachoUpdateSec = top[FPSTR(_tachoUpdateSec)] | tachoUpdateSec;
281+
tachoUpdateSec = (uint8_t) max(1,(int)tachoUpdateSec); // bounds checking
282+
targetTemperature = top[FPSTR(_temperature)] | targetTemperature;
283+
284+
if (!initDone) {
285+
// first run: reading from cfg.json
286+
tachoPin = newTachoPin;
287+
pwmPin = newPwmPin;
288+
DEBUG_PRINTLN(F(" config loaded."));
289+
} else {
290+
DEBUG_PRINTLN(F(" config (re)loaded."));
291+
// changing paramters from settings page
292+
if (tachoPin != newTachoPin || pwmPin != newPwmPin) {
293+
DEBUG_PRINTLN(F("Re-init pins."));
294+
// deallocate pin and release interrupts
295+
deinitTacho();
296+
deinitPWMfan();
297+
tachoPin = newTachoPin;
298+
pwmPin = newPwmPin;
299+
// initialise
300+
setup();
301+
}
302+
}
303+
304+
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
305+
return !top[FPSTR(_enabled)].isNull();
306+
}
307+
308+
/*
309+
* getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
310+
* This could be used in the future for the system to determine whether your usermod is installed.
311+
*/
312+
uint16_t getId() {
313+
return USERMOD_ID_PWM_FAN;
314+
}
315+
};
316+
317+
// strings to reduce flash memory usage (used more than twice)
318+
const char PWMFanUsermod::_name[] PROGMEM = "PWM-fan";
319+
const char PWMFanUsermod::_enabled[] PROGMEM = "enabled";
320+
const char PWMFanUsermod::_tachoPin[] PROGMEM = "tacho-pin";
321+
const char PWMFanUsermod::_pwmPin[] PROGMEM = "PWM-pin";
322+
const char PWMFanUsermod::_temperature[] PROGMEM = "target-temp-C";
323+
const char PWMFanUsermod::_tachoUpdateSec[] PROGMEM = "tacho-update-s";

wled00/const.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
#define USERMOD_ID_ELEKSTUBE_IPS 16 //Usermod "usermod_elekstube_ips.h"
6060
#define USERMOD_ID_SN_PHOTORESISTOR 17 //Usermod "usermod_sn_photoresistor.h"
6161
#define USERMOD_ID_BATTERY_STATUS_BASIC 18 //Usermod "usermod_v2_battery_status_basic.h"
62+
#define USERMOD_ID_PWM_FAN 19 //Usermod "usermod_PWM_fan.h"
6263

6364
//Access point behavior
6465
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot

wled00/usermods_list.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
#include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h"
2424
#endif
2525

26-
//#include "usermod_v2_empty.h"
26+
#ifdef USERMOD_PWM_FAN
27+
#include "../usermods/PWM_fan/usermod_PWM_fan.h"
28+
#endif
2729

2830
#ifdef USERMOD_BUZZER
2931
#include "../usermods/buzzer/usermod_v2_buzzer.h"
@@ -115,7 +117,9 @@ void registerUsermods()
115117
usermods.add(new Usermod_SN_Photoresistor());
116118
#endif
117119

118-
//usermods.add(new UsermodRenameMe());
120+
#ifdef USERMOD_PWM_FAN
121+
usermods.add(new PWMFanUsermod());
122+
#endif
119123

120124
#ifdef USERMOD_BUZZER
121125
usermods.add(new BuzzerUsermod());

0 commit comments

Comments
 (0)