#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 #ifndef TACHO_PIN #define TACHO_PIN -1 #endif #ifndef PWM_PIN #define PWM_PIN -1 #endif // 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 bool lockFan = false; #ifdef USERMOD_DALLASTEMPERATURE UsermodTemperature* tempUM; #endif // configurable parameters int8_t tachoPin = TACHO_PIN; int8_t pwmPin = PWM_PIN; uint8_t tachoUpdateSec = 30; float targetTemperature = 25.0; 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. uint8_t pwmValuePct = 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[]; static const char _minPWMValuePct[]; static const char _IRQperRotation[]; static const char _speed[]; static const char _lock[]; 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) { // store milliseconds when tacho was measured the last time msLastTachoMeasurement = millis(); 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; // 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; int pwmStep = ((100 - minPWMValuePct) * newPWMvalue) / (7*100); int pwmMinimumValue = (minPWMValuePct * newPWMvalue) / 100; if ((temp == NAN) || (temp <= -100.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 = pwmMinimumValue + pwmStep; } else if (difftemp <= 1.0) { newPWMvalue = pwmMinimumValue + 2*pwmStep; } else if (difftemp <= 1.5) { newPWMvalue = pwmMinimumValue + 3*pwmStep; } else if (difftemp <= 2.0) { newPWMvalue = pwmMinimumValue + 4*pwmStep; } else if (difftemp <= 2.5) { newPWMvalue = pwmMinimumValue + 5*pwmStep; } else if (difftemp <= 3.0) { newPWMvalue = pwmMinimumValue + 6*pwmStep; } 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((minPWMValuePct * 255) / 100); // inital fan speed 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(); if (!lockFan) 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 (enabled) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); // if (!tempUM) { JsonArray infoArr = user.createNestedArray(F("Fan speed [%]")); String uiDomString = F("
"); // infoArr.add(uiDomString); // } if (tachoPin >= 0) { JsonArray data = user.createNestedArray(FPSTR(_name)); data.add(last_rpm); data.add(F("rpm")); } else { JsonArray data = user.createNestedArray(FPSTR(_name)); if (lockFan) data.add(F("locked")); else data.add(F("auto")); } } } /* * 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 || !enabled) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { if (!usermod[FPSTR(_speed)].isNull() && usermod[FPSTR(_speed)].is