From b003ed3f03a0b7fed643b9cf3c462fbaf6e19063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 3 Oct 2021 23:34:21 +0200 Subject: [PATCH] PWM fan with temperature control usermod (#2246) * PWM fan with temperature control usermod * Fix for incorrect RPM reported. --- usermods/PWM_fan/readme.md | 36 ++++ usermods/PWM_fan/usermod_PWM_fan.h | 323 +++++++++++++++++++++++++++++ wled00/const.h | 1 + wled00/usermods_list.cpp | 8 +- 4 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 usermods/PWM_fan/readme.md create mode 100644 usermods/PWM_fan/usermod_PWM_fan.h diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md new file mode 100644 index 00000000..a40098c1 --- /dev/null +++ b/usermods/PWM_fan/readme.md @@ -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 diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h new file mode 100644 index 00000000..2dbbfe3b --- /dev/null +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -0,0 +1,323 @@ +#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); + last_rpm /= tachoUpdateSec; + // 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"; diff --git a/wled00/const.h b/wled00/const.h index e5061627..309e3959 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -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 diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 475f3434..20bcf2b9 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -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());