PWM fan with temperature control usermod

This commit is contained in:
Blaz Kristan 2021-10-03 10:33:17 +02:00
parent 05b532b9eb
commit 772c80aa85
4 changed files with 365 additions and 2 deletions

View File

@ -0,0 +1,36 @@
# PWM fan
v2 Usermod to to control PWM fan with RPM feedback and temperature control
This usermod requires Dallas Temperature usermod to obtain temperature information. If this is not available the fan will always run at 100% speed.
If the fan does not have _tacho_ (RPM) output you can set the _tacho-pin_ to -1 to not use that feature.
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%.
If the _tacho_ is supported the current speed (in RPM) will be repored in WLED Info page.
## Installation
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`.
You will also need `-D USERMOD_DALLASTEMPERATURE`.
### Define Your Options
All of the parameters are configured during run-time using Usermods settings page.
This includes:
* PWM output pin
* tacho input pin
* sampling frequency in seconds
* threshold temperature in degees C
_NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency.
### PlatformIO requirements
No special requirements.
## Change Log
2021-10
* First public release

View File

@ -0,0 +1,322 @@
#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
// 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;
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.
const int pwmMinimumValue = 120;
const int pwmStep = 10;
unsigned long msLastTachoMeasurement = 0;
uint16_t last_rpm = 0;
#ifdef ARDUINO_ARCH_ESP32
uint8_t pwmChannel = 255;
#endif
#ifdef USERMOD_DALLASTEMPERATURE
UsermodTemperature* tempUM;
#endif
// configurable parameters
int8_t tachoPin = -1;
int8_t pwmPin = -1;
uint8_t tachoUpdateSec = 30;
float targetTemperature = 25.0;
// 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[];
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) {
if (tachoPin < 0) return;
// start of tacho measurement
// detach interrupt while calculating rpm
detachInterrupt(digitalPinToInterrupt(tachoPin));
// calculate rpm
last_rpm = counter_rpm * (60 / numberOfInterrupsInOneSingleRotation);
// reset counter
counter_rpm = 0;
// store milliseconds when tacho was measured the last time
msLastTachoMeasurement = millis();
// 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)) {
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;
if ((temp == NAN) || (temp <= 0.0)) {
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.
newPWMvalue = pwmMinimumValue;
} else if (difftemp <= 0.5) {
newPWMvalue = 140;
} else if (difftemp <= 1.0) {
newPWMvalue = 160;
} else if (difftemp <= 1.5) {
newPWMvalue = 180;
} else if (difftemp <= 2.0) {
newPWMvalue = 200;
} else if (difftemp <= 2.5) {
newPWMvalue = 220;
} else if (difftemp <= 3.0) {
newPWMvalue = 240;
}
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();
updateFanSpeed(pwmMinimumValue);
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();
setFanPWMbasedOnTemperature();
}
/*
* 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) {
if (tachoPin < 0) return;
JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject("u");
JsonArray data = user.createNestedArray(FPSTR(_name));
data.add(last_rpm);
data.add(F("rpm"));
}
/*
* 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
*/
//void readFromJsonState(JsonObject& root) {
// if (!initDone) return; // prevent crash on boot applyPreset()
//}
/*
* 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;
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;
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
return !top[FPSTR(_enabled)].isNull();
}
/*
* 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";

View File

@ -59,6 +59,7 @@
#define USERMOD_ID_ELEKSTUBE_IPS 16 //Usermod "usermod_elekstube_ips.h"
#define USERMOD_ID_SN_PHOTORESISTOR 17 //Usermod "usermod_sn_photoresistor.h"
#define USERMOD_ID_BATTERY_STATUS_BASIC 18 //Usermod "usermod_v2_battery_status_basic.h"
#define USERMOD_ID_PWM_FAN 19 //Usermod "usermod_PWM_fan.h"
//Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot

View File

@ -23,7 +23,9 @@
#include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h"
#endif
//#include "usermod_v2_empty.h"
#ifdef USERMOD_PWM_FAN
#include "../usermods/PWM_fan/usermod_PWM_fan.h"
#endif
#ifdef USERMOD_BUZZER
#include "../usermods/buzzer/usermod_v2_buzzer.h"
@ -115,7 +117,9 @@ void registerUsermods()
usermods.add(new Usermod_SN_Photoresistor());
#endif
//usermods.add(new UsermodRenameMe());
#ifdef USERMOD_PWM_FAN
usermods.add(new PWMFanUsermod());
#endif
#ifdef USERMOD_BUZZER
usermods.add(new BuzzerUsermod());