2021-10-03 10:33:17 +02:00
# pragma once
# ifndef USERMOD_DALLASTEMPERATURE
# error The "PWM fan" usermod requires "Dallas Temeprature" usermod to function properly.
# endif
# include "wled.h"
// PWM & tacho code curtesy of @KlausMu
// https://github.com/KlausMu/esp32-fan-controller/tree/main/src
// adapted for WLED usermod by @blazoncek
2022-04-29 09:52:45 +02:00
# ifndef TACHO_PIN
# define TACHO_PIN -1
# endif
# ifndef PWM_PIN
# define PWM_PIN -1
# endif
2021-10-03 10:33:17 +02:00
// tacho counter
static volatile unsigned long counter_rpm = 0 ;
// Interrupt counting every rotation of the fan
// https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/
static void IRAM_ATTR rpm_fan ( ) {
counter_rpm + + ;
}
class PWMFanUsermod : public Usermod {
private :
bool initDone = false ;
bool enabled = true ;
unsigned long msLastTachoMeasurement = 0 ;
uint16_t last_rpm = 0 ;
# ifdef ARDUINO_ARCH_ESP32
uint8_t pwmChannel = 255 ;
# endif
2022-05-18 19:46:31 +02:00
bool lockFan = false ;
2021-10-03 10:33:17 +02:00
# ifdef USERMOD_DALLASTEMPERATURE
UsermodTemperature * tempUM ;
# endif
// configurable parameters
2022-04-29 09:52:45 +02:00
int8_t tachoPin = TACHO_PIN ;
int8_t pwmPin = PWM_PIN ;
2021-10-03 10:33:17 +02:00
uint8_t tachoUpdateSec = 30 ;
float targetTemperature = 25.0 ;
2021-10-04 11:48:03 +02:00
uint8_t minPWMValuePct = 50 ;
uint8_t numberOfInterrupsInOneSingleRotation = 2 ; // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups.
2022-08-16 20:57:24 +02:00
uint8_t pwmValuePct = 0 ;
2021-10-03 10:33:17 +02:00
// strings to reduce flash memory usage (used more than twice)
static const char _name [ ] ;
static const char _enabled [ ] ;
static const char _tachoPin [ ] ;
static const char _pwmPin [ ] ;
static const char _temperature [ ] ;
static const char _tachoUpdateSec [ ] ;
2021-10-04 11:48:03 +02:00
static const char _minPWMValuePct [ ] ;
static const char _IRQperRotation [ ] ;
2022-05-18 19:46:31 +02:00
static const char _speed [ ] ;
static const char _lock [ ] ;
2021-10-03 10:33:17 +02:00
void initTacho ( void ) {
if ( tachoPin < 0 | | ! pinManager . allocatePin ( tachoPin , false , PinOwner : : UM_Unspecified ) ) {
tachoPin = - 1 ;
return ;
}
pinMode ( tachoPin , INPUT ) ;
digitalWrite ( tachoPin , HIGH ) ;
attachInterrupt ( digitalPinToInterrupt ( tachoPin ) , rpm_fan , FALLING ) ;
DEBUG_PRINTLN ( F ( " Tacho sucessfully initialized. " ) ) ;
}
void deinitTacho ( void ) {
if ( tachoPin < 0 ) return ;
detachInterrupt ( digitalPinToInterrupt ( tachoPin ) ) ;
pinManager . deallocatePin ( tachoPin , PinOwner : : UM_Unspecified ) ;
tachoPin = - 1 ;
}
void updateTacho ( void ) {
2022-08-16 20:57:24 +02:00
// store milliseconds when tacho was measured the last time
msLastTachoMeasurement = millis ( ) ;
2021-10-03 10:33:17 +02:00
if ( tachoPin < 0 ) return ;
// start of tacho measurement
// detach interrupt while calculating rpm
detachInterrupt ( digitalPinToInterrupt ( tachoPin ) ) ;
// calculate rpm
2021-10-04 11:48:03 +02:00
last_rpm = ( counter_rpm * 60 ) / numberOfInterrupsInOneSingleRotation ;
2021-10-03 14:01:05 +02:00
last_rpm / = tachoUpdateSec ;
2021-10-03 10:33:17 +02:00
// reset counter
counter_rpm = 0 ;
// attach interrupt again
attachInterrupt ( digitalPinToInterrupt ( tachoPin ) , rpm_fan , FALLING ) ;
}
// https://randomnerdtutorials.com/esp32-pwm-arduino-ide/
void initPWMfan ( void ) {
if ( pwmPin < 0 | | ! pinManager . allocatePin ( pwmPin , true , PinOwner : : UM_Unspecified ) ) {
2022-08-19 21:25:44 +02:00
enabled = false ;
2021-10-03 10:33:17 +02:00
pwmPin = - 1 ;
return ;
}
# ifdef ESP8266
analogWriteRange ( 255 ) ;
analogWriteFreq ( WLED_PWM_FREQ ) ;
# else
pwmChannel = pinManager . allocateLedc ( 1 ) ;
if ( pwmChannel = = 255 ) { //no more free LEDC channels
deinitPWMfan ( ) ; return ;
}
// configure LED PWM functionalitites
ledcSetup ( pwmChannel , 25000 , 8 ) ;
// attach the channel to the GPIO to be controlled
ledcAttachPin ( pwmPin , pwmChannel ) ;
# endif
DEBUG_PRINTLN ( F ( " Fan PWM sucessfully initialized. " ) ) ;
}
void deinitPWMfan ( void ) {
if ( pwmPin < 0 ) return ;
pinManager . deallocatePin ( pwmPin , PinOwner : : UM_Unspecified ) ;
# ifdef ARDUINO_ARCH_ESP32
pinManager . deallocateLedc ( pwmChannel , 1 ) ;
# endif
pwmPin = - 1 ;
}
void updateFanSpeed ( uint8_t pwmValue ) {
if ( pwmPin < 0 ) return ;
# ifdef ESP8266
analogWrite ( pwmPin , pwmValue ) ;
# else
ledcWrite ( pwmChannel , pwmValue ) ;
# endif
}
float getActualTemperature ( void ) {
# ifdef USERMOD_DALLASTEMPERATURE
if ( tempUM ! = nullptr )
return tempUM - > getTemperatureC ( ) ;
# endif
return - 127.0f ;
}
void setFanPWMbasedOnTemperature ( void ) {
float temp = getActualTemperature ( ) ;
float difftemp = temp - targetTemperature ;
// Default to run fan at full speed.
int newPWMvalue = 255 ;
2021-10-04 11:48:03 +02:00
int pwmStep = ( ( 100 - minPWMValuePct ) * newPWMvalue ) / ( 7 * 100 ) ;
int pwmMinimumValue = ( minPWMValuePct * newPWMvalue ) / 100 ;
2021-10-03 10:33:17 +02:00
2021-12-28 21:53:29 +01:00
if ( ( temp = = NAN ) | | ( temp < = - 100.0 ) ) {
2021-10-03 10:33:17 +02:00
DEBUG_PRINTLN ( F ( " WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255. " ) ) ;
} else if ( difftemp < = 0.0 ) {
// Temperature is below target temperature. Run fan at minimum speed.
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue ;
2021-10-03 10:33:17 +02:00
} else if ( difftemp < = 0.5 ) {
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue + pwmStep ;
2021-10-03 10:33:17 +02:00
} else if ( difftemp < = 1.0 ) {
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue + 2 * pwmStep ;
2021-10-03 10:33:17 +02:00
} else if ( difftemp < = 1.5 ) {
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue + 3 * pwmStep ;
2021-10-03 10:33:17 +02:00
} else if ( difftemp < = 2.0 ) {
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue + 4 * pwmStep ;
2021-10-03 10:33:17 +02:00
} else if ( difftemp < = 2.5 ) {
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue + 5 * pwmStep ;
2021-10-03 10:33:17 +02:00
} else if ( difftemp < = 3.0 ) {
2021-10-04 11:48:03 +02:00
newPWMvalue = pwmMinimumValue + 6 * pwmStep ;
2021-10-03 10:33:17 +02:00
}
updateFanSpeed ( newPWMvalue ) ;
}
public :
// gets called once at boot. Do all initialization that doesn't depend on
// network here
void setup ( ) {
# ifdef USERMOD_DALLASTEMPERATURE
// This Usermod requires Temperature usermod
tempUM = ( UsermodTemperature * ) usermods . lookup ( USERMOD_ID_TEMPERATURE ) ;
# endif
initTacho ( ) ;
initPWMfan ( ) ;
2021-10-04 11:48:03 +02:00
updateFanSpeed ( ( minPWMValuePct * 255 ) / 100 ) ; // inital fan speed
2021-10-03 10:33:17 +02:00
initDone = true ;
}
// gets called every time WiFi is (re-)connected. Initialize own network
// interfaces here
void connected ( ) { }
/*
* Da loop .
*/
void loop ( ) {
if ( ! enabled | | strip . isUpdating ( ) ) return ;
unsigned long now = millis ( ) ;
if ( ( now - msLastTachoMeasurement ) < ( tachoUpdateSec * 1000 ) ) return ;
updateTacho ( ) ;
2022-05-18 19:46:31 +02:00
if ( ! lockFan ) setFanPWMbasedOnTemperature ( ) ;
2021-10-03 10:33:17 +02:00
}
/*
* addToJsonInfo ( ) can be used to add custom entries to the / json / info part of the JSON API .
* Creating an " u " object allows you to add custom key / value pairs to the Info section of the WLED web UI .
* Below it is shown how this could be used for e . g . a light sensor
*/
void addToJsonInfo ( JsonObject & root ) {
2022-08-19 21:25:44 +02:00
JsonObject user = root [ " u " ] ;
if ( user . isNull ( ) ) user = root . createNestedObject ( " u " ) ;
JsonArray infoArr = user . createNestedArray ( FPSTR ( _name ) ) ;
String uiDomString = F ( " <button class= \" btn btn-xs \" onclick= \" requestJson({' " ) ;
uiDomString + = FPSTR ( _name ) ;
uiDomString + = F ( " ':{' " ) ;
uiDomString + = FPSTR ( _enabled ) ;
uiDomString + = F ( " ': " ) ;
uiDomString + = enabled ? " false " : " true " ;
uiDomString + = F ( " }}); \" ><i class= \" icons " ) ;
uiDomString + = enabled ? " on " : " off " ;
uiDomString + = F ( " \" ></i></button> " ) ;
infoArr . add ( uiDomString ) ;
2022-08-16 20:57:24 +02:00
if ( enabled ) {
2022-08-19 21:25:44 +02:00
JsonArray infoArr = user . createNestedArray ( F ( " Manual " ) ) ;
2022-08-16 20:57:24 +02:00
String uiDomString = F ( " <div class= \" slider \" ><div class= \" sliderwrap il \" ><input class= \" noslide \" onchange= \" requestJson({' " ) ;
uiDomString + = FPSTR ( _name ) ;
uiDomString + = F ( " ':{' " ) ;
uiDomString + = FPSTR ( _speed ) ;
uiDomString + = F ( " ':parseInt(this.value)}}); \" oninput= \" updateTrail(this); \" max=100 min=0 type= \" range \" value= " ) ;
uiDomString + = pwmValuePct ;
uiDomString + = F ( " /><div class= \" sliderdisplay \" ></div></div></div> " ) ; //<output class=\"sliderbubble\"></output>
infoArr . add ( uiDomString ) ;
2022-08-19 21:25:44 +02:00
JsonArray data = user . createNestedArray ( F ( " Speed " ) ) ;
2022-08-16 20:57:24 +02:00
if ( tachoPin > = 0 ) {
data . add ( last_rpm ) ;
data . add ( F ( " rpm " ) ) ;
} else {
if ( lockFan ) data . add ( F ( " locked " ) ) ;
else data . add ( F ( " auto " ) ) ;
}
}
2021-10-03 10:33:17 +02:00
}
/*
* addToJsonState ( ) can be used to add custom entries to the / json / state part of the JSON API ( state object ) .
* Values in the state object may be modified by connected clients
*/
//void addToJsonState(JsonObject& root) {
//}
/*
* readFromJsonState ( ) can be used to receive data clients send to the / json / state part of the JSON API ( state object ) .
* Values in the state object may be modified by connected clients
*/
2022-05-18 19:46:31 +02:00
void readFromJsonState ( JsonObject & root ) {
2022-08-19 21:25:44 +02:00
if ( ! initDone ) return ; // prevent crash on boot applyPreset()
2022-05-18 19:46:31 +02:00
JsonObject usermod = root [ FPSTR ( _name ) ] ;
if ( ! usermod . isNull ( ) ) {
2022-08-19 21:25:44 +02:00
if ( usermod [ FPSTR ( _enabled ) ] . is < bool > ( ) ) {
enabled = usermod [ FPSTR ( _enabled ) ] . as < bool > ( ) ;
if ( ! enabled ) updateFanSpeed ( 0 ) ;
}
if ( enabled & & ! usermod [ FPSTR ( _speed ) ] . isNull ( ) & & usermod [ FPSTR ( _speed ) ] . is < int > ( ) ) {
pwmValuePct = usermod [ FPSTR ( _speed ) ] . as < int > ( ) ;
updateFanSpeed ( ( constrain ( pwmValuePct , 0 , 100 ) * 255 ) / 100 ) ;
if ( pwmValuePct ) lockFan = true ;
2022-05-18 19:46:31 +02:00
}
2022-08-19 21:25:44 +02:00
if ( enabled & & ! usermod [ FPSTR ( _lock ) ] . isNull ( ) & & usermod [ FPSTR ( _lock ) ] . is < bool > ( ) ) {
2022-05-18 19:46:31 +02:00
lockFan = usermod [ FPSTR ( _lock ) ] . as < bool > ( ) ;
}
}
}
2021-10-03 10:33:17 +02:00
/*
* addToConfig ( ) can be used to add custom persistent settings to the cfg . json file in the " um " ( usermod ) object .
* It will be called by WLED when settings are actually saved ( for example , LED settings are saved )
* If you want to force saving the current state , use serializeConfig ( ) in your loop ( ) .
*
* CAUTION : serializeConfig ( ) will initiate a filesystem write operation .
* It might cause the LEDs to stutter and will cause flash wear if called too often .
* Use it sparingly and always in the loop , never in network callbacks !
*
* addToConfig ( ) will also not yet add your setting to one of the settings pages automatically .
* To make that work you still have to add the setting to the HTML , xml . cpp and set . cpp manually .
*
* I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings !
*/
void addToConfig ( JsonObject & root ) {
JsonObject top = root . createNestedObject ( FPSTR ( _name ) ) ; // usermodname
top [ FPSTR ( _enabled ) ] = enabled ;
top [ FPSTR ( _pwmPin ) ] = pwmPin ;
top [ FPSTR ( _tachoPin ) ] = tachoPin ;
top [ FPSTR ( _tachoUpdateSec ) ] = tachoUpdateSec ;
top [ FPSTR ( _temperature ) ] = targetTemperature ;
2021-10-04 11:48:03 +02:00
top [ FPSTR ( _minPWMValuePct ) ] = minPWMValuePct ;
top [ FPSTR ( _IRQperRotation ) ] = numberOfInterrupsInOneSingleRotation ;
2021-10-03 10:33:17 +02:00
DEBUG_PRINTLN ( F ( " Autosave config saved. " ) ) ;
}
/*
* readFromConfig ( ) can be used to read back the custom settings you added with addToConfig ( ) .
* This is called by WLED when settings are loaded ( currently this only happens once immediately after boot )
*
* readFromConfig ( ) is called BEFORE setup ( ) . This means you can use your persistent values in setup ( ) ( e . g . pin assignments , buffer sizes ) ,
* 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 .
* If you don ' t know what that is , don ' t fret . It most likely doesn ' t affect your use case : )
*
* The function should return true if configuration was successfully loaded or false if there was no configuration .
*/
bool readFromConfig ( JsonObject & root ) {
int8_t newTachoPin = tachoPin ;
int8_t newPwmPin = pwmPin ;
JsonObject top = root [ FPSTR ( _name ) ] ;
DEBUG_PRINT ( FPSTR ( _name ) ) ;
if ( top . isNull ( ) ) {
DEBUG_PRINTLN ( F ( " : No config found. (Using defaults.) " ) ) ;
return false ;
}
enabled = top [ FPSTR ( _enabled ) ] | enabled ;
newTachoPin = top [ FPSTR ( _tachoPin ) ] | newTachoPin ;
newPwmPin = top [ FPSTR ( _pwmPin ) ] | newPwmPin ;
tachoUpdateSec = top [ FPSTR ( _tachoUpdateSec ) ] | tachoUpdateSec ;
tachoUpdateSec = ( uint8_t ) max ( 1 , ( int ) tachoUpdateSec ) ; // bounds checking
targetTemperature = top [ FPSTR ( _temperature ) ] | targetTemperature ;
2021-10-04 11:48:03 +02:00
minPWMValuePct = top [ FPSTR ( _minPWMValuePct ) ] | minPWMValuePct ;
minPWMValuePct = ( uint8_t ) min ( 100 , max ( 0 , ( int ) minPWMValuePct ) ) ; // bounds checking
numberOfInterrupsInOneSingleRotation = top [ FPSTR ( _IRQperRotation ) ] | numberOfInterrupsInOneSingleRotation ;
numberOfInterrupsInOneSingleRotation = ( uint8_t ) max ( 1 , ( int ) numberOfInterrupsInOneSingleRotation ) ; // bounds checking
2021-10-03 10:33:17 +02:00
if ( ! initDone ) {
// first run: reading from cfg.json
tachoPin = newTachoPin ;
pwmPin = newPwmPin ;
DEBUG_PRINTLN ( F ( " config loaded. " ) ) ;
} else {
DEBUG_PRINTLN ( F ( " config (re)loaded. " ) ) ;
// changing paramters from settings page
if ( tachoPin ! = newTachoPin | | pwmPin ! = newPwmPin ) {
DEBUG_PRINTLN ( F ( " Re-init pins. " ) ) ;
// deallocate pin and release interrupts
deinitTacho ( ) ;
deinitPWMfan ( ) ;
tachoPin = newTachoPin ;
pwmPin = newPwmPin ;
// initialise
setup ( ) ;
}
}
// use "return !top["newestParameter"].isNull();" when updating Usermod with new features
2021-10-04 11:48:03 +02:00
return ! top [ FPSTR ( _IRQperRotation ) ] . isNull ( ) ;
2021-10-03 10:33:17 +02:00
}
/*
* getId ( ) allows you to optionally give your V2 usermod an unique ID ( please define it in const . h ! ) .
* This could be used in the future for the system to determine whether your usermod is installed .
*/
uint16_t getId ( ) {
return USERMOD_ID_PWM_FAN ;
}
} ;
// strings to reduce flash memory usage (used more than twice)
const char PWMFanUsermod : : _name [ ] PROGMEM = " PWM-fan " ;
const char PWMFanUsermod : : _enabled [ ] PROGMEM = " enabled " ;
const char PWMFanUsermod : : _tachoPin [ ] PROGMEM = " tacho-pin " ;
const char PWMFanUsermod : : _pwmPin [ ] PROGMEM = " PWM-pin " ;
const char PWMFanUsermod : : _temperature [ ] PROGMEM = " target-temp-C " ;
const char PWMFanUsermod : : _tachoUpdateSec [ ] PROGMEM = " tacho-update-s " ;
2021-10-04 11:48:03 +02:00
const char PWMFanUsermod : : _minPWMValuePct [ ] PROGMEM = " min-PWM-percent " ;
const char PWMFanUsermod : : _IRQperRotation [ ] PROGMEM = " IRQs-per-rotation " ;
2022-05-18 19:46:31 +02:00
const char PWMFanUsermod : : _speed [ ] PROGMEM = " speed " ;
const char PWMFanUsermod : : _lock [ ] PROGMEM = " lock " ;