diff --git a/usermods/GPU_Fan_Controller/GPU_Fan_Controller.cpp b/usermods/GPU_Fan_Controller/GPU_Fan_Controller.cpp index 1b6227a1..d457c9aa 100644 --- a/usermods/GPU_Fan_Controller/GPU_Fan_Controller.cpp +++ b/usermods/GPU_Fan_Controller/GPU_Fan_Controller.cpp @@ -1,44 +1,16 @@ #include "wled.h" #include "gpu_fan_html.h" -/* - * GPU Fan Controller Usermod for WLED - * - * This usermod controls a PWM fan based on GPU temperature received via HTTP API - * from a Python monitoring script. It supports fixed speed mode and temperature - * curve-based control. - * - * Features: - * - Web API for temperature updates from external sources (GPU, etc.) - * - Fixed speed mode with configurable percentage - * - Temperature curve mode with up to 5 configurable points - * - Visual curve editor at /gpu-fan - * - Safety fallback to 100% if temperature data times out - * - Integration with WLED's web interface - * - * Connections: - * - Fan GND -> ESP32 GND - * - Fan +12V -> 12V Power Supply - * - Fan PWM -> Configured GPIO (default: GPIO 13) - * - * API Endpoints: - * - POST /json/state with JSON: {"GPU-Fan": {"temperature": 65.5}} - * - GET /json/info - * - GET /gpu-fan (curve editor page) - */ - #ifndef PWM_FAN_PIN #define PWM_FAN_PIN 13 #endif -// PWM configuration for 4-pin PC fans (Intel spec: 25kHz) #define GPU_FAN_PWM_FREQ 25000 #define GPU_FAN_PWM_RESOLUTION 8 class GPUFanControllerUsermod : public Usermod { private: - // Configuration bool enabled = true; bool initDone = false; bool webHandlerRegistered = false; @@ -48,53 +20,43 @@ class GPUFanControllerUsermod : public Usermod { uint8_t pwmChannel = 255; #endif - // Control modes enum ControlMode { MODE_FIXED = 0, MODE_CURVE = 1 }; - // Fan configuration ControlMode controlMode = MODE_CURVE; - uint8_t fixedSpeedPct = 50; // 0-100% + uint8_t fixedSpeedPct = 50; - // Temperature curve - using flat structure for WLED config compatibility - // 5 curve points with separate temp and speed values static const uint8_t MAX_CURVE_POINTS = 5; uint8_t curveCount = 4; - // Curve point temperatures (°C) int16_t curveTemp1 = 30; int16_t curveTemp2 = 50; int16_t curveTemp3 = 70; int16_t curveTemp4 = 85; int16_t curveTemp5 = 95; - // Curve point speeds (%) uint8_t curveSpeed1 = 30; uint8_t curveSpeed2 = 50; uint8_t curveSpeed3 = 75; uint8_t curveSpeed4 = 100; uint8_t curveSpeed5 = 100; - // Runtime state float currentTemp = 25.0f; uint8_t currentPWM = 0; unsigned long lastTempUpdate = 0; - unsigned long tempTimeoutMs = 10000; // 10 second timeout + unsigned long tempTimeoutMs = 10000; unsigned long lastLoopTime = 0; - unsigned long loopIntervalMs = 2000; // Update every 2 seconds + unsigned long loopIntervalMs = 2000; - // String constants static const char _name[]; static const char _enabled[]; - // Initialize PWM void initPWM() { if (pwmPin < 0 || !PinManager::allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { enabled = false; pwmPin = -1; - DEBUG_PRINTLN(F("GPU Fan: PWM pin allocation failed")); return; } @@ -105,20 +67,15 @@ class GPUFanControllerUsermod : public Usermod { pwmChannel = PinManager::allocateLedc(1); if (pwmChannel == 255) { deinitPWM(); - DEBUG_PRINTLN(F("GPU Fan: LEDC channel allocation failed")); return; } ledcSetup(pwmChannel, GPU_FAN_PWM_FREQ, GPU_FAN_PWM_RESOLUTION); ledcAttachPin(pwmPin, pwmChannel); #endif - - DEBUG_PRINTLN(F("GPU Fan: PWM initialized successfully")); } - // Deinitialize PWM void deinitPWM() { if (pwmPin < 0) return; - PinManager::deallocatePin(pwmPin, PinOwner::UM_Unspecified); #ifdef ARDUINO_ARCH_ESP32 if (pwmChannel != 255) { @@ -129,10 +86,8 @@ class GPUFanControllerUsermod : public Usermod { pwmPin = -1; } - // Set fan speed (0-255 PWM value) void setFanPWM(uint8_t pwmValue) { if (!enabled || pwmPin < 0) return; - currentPWM = pwmValue; #ifdef ESP8266 analogWrite(pwmPin, pwmValue); @@ -141,7 +96,6 @@ class GPUFanControllerUsermod : public Usermod { #endif } - // Get curve temperature at index int16_t getCurveTemp(uint8_t idx) { switch(idx) { case 0: return curveTemp1; @@ -153,7 +107,6 @@ class GPUFanControllerUsermod : public Usermod { } } - // Get curve speed at index uint8_t getCurveSpeed(uint8_t idx) { switch(idx) { case 0: return curveSpeed1; @@ -165,21 +118,12 @@ class GPUFanControllerUsermod : public Usermod { } } - // Calculate fan speed from temperature curve uint8_t calculateCurveSpeed(float temp) { - if (curveCount < 2) return 50; // Fallback + if (curveCount < 2) return 50; - // Below first point - if (temp <= getCurveTemp(0)) { - return getCurveSpeed(0); - } + if (temp <= getCurveTemp(0)) return getCurveSpeed(0); + if (temp >= getCurveTemp(curveCount - 1)) return getCurveSpeed(curveCount - 1); - // Above last point - if (temp >= getCurveTemp(curveCount - 1)) { - return getCurveSpeed(curveCount - 1); - } - - // Linear interpolation between points for (uint8_t i = 0; i < curveCount - 1; i++) { int16_t t1 = getCurveTemp(i); int16_t t2 = getCurveTemp(i + 1); @@ -187,40 +131,19 @@ class GPUFanControllerUsermod : public Usermod { uint8_t s2 = getCurveSpeed(i + 1); if (temp >= t1 && temp <= t2) { - float tempRange = t2 - t1; - float speedRange = s2 - s1; - float tempDiff = temp - t1; - - int speed = s1 + (int)((tempDiff / tempRange) * speedRange); - return constrain(speed, 0, 100); + float ratio = (temp - t1) / (float)(t2 - t1); + return s1 + (uint8_t)(ratio * (s2 - s1)); } } - - return 50; // Fallback + return 50; } - // Update fan speed based on current mode and temperature void updateFanSpeed() { - uint8_t targetSpeedPct; - - if (controlMode == MODE_FIXED) { - targetSpeedPct = fixedSpeedPct; - } else { - targetSpeedPct = calculateCurveSpeed(currentTemp); - } - - // Convert percentage to PWM (0-255) + uint8_t targetSpeedPct = (controlMode == MODE_FIXED) ? fixedSpeedPct : calculateCurveSpeed(currentTemp); uint8_t targetPWM = map(constrain(targetSpeedPct, 0, 100), 0, 100, 0, 255); - - if (targetPWM != currentPWM) { - setFanPWM(targetPWM); - DEBUG_PRINTF("GPU Fan: %d%% (PWM: %d) | Temp: %.1f°C | Mode: %s\n", - targetSpeedPct, currentPWM, currentTemp, - controlMode == MODE_FIXED ? "Fixed" : "Curve"); - } + if (targetPWM != currentPWM) setFanPWM(targetPWM); } - // Register web handler - call this once void registerWebHandler() { if (webHandlerRegistered) return; @@ -228,26 +151,50 @@ class GPUFanControllerUsermod : public Usermod { request->send_P(200, "text/html", GPU_FAN_HTML); }); + // Custom save endpoint + server.on("/gpu-fan/save", HTTP_POST, + [](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"success\":true}"); + }, + NULL, + [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + DynamicJsonDocument doc(1024); + if (deserializeJson(doc, (const char*)data, len)) return; + + if (doc.containsKey("mode")) controlMode = (ControlMode)doc["mode"].as(); + if (doc.containsKey("fixed-speed")) fixedSpeedPct = doc["fixed-speed"].as(); + if (doc.containsKey("curve-points")) curveCount = constrain(doc["curve-points"].as(), 2, 5); + + if (doc.containsKey("curve-t1")) curveTemp1 = doc["curve-t1"].as(); + if (doc.containsKey("curve-t2")) curveTemp2 = doc["curve-t2"].as(); + if (doc.containsKey("curve-t3")) curveTemp3 = doc["curve-t3"].as(); + if (doc.containsKey("curve-t4")) curveTemp4 = doc["curve-t4"].as(); + if (doc.containsKey("curve-t5")) curveTemp5 = doc["curve-t5"].as(); + if (doc.containsKey("curve-s1")) curveSpeed1 = constrain(doc["curve-s1"].as(), 0, 100); + if (doc.containsKey("curve-s2")) curveSpeed2 = constrain(doc["curve-s2"].as(), 0, 100); + if (doc.containsKey("curve-s3")) curveSpeed3 = constrain(doc["curve-s3"].as(), 0, 100); + if (doc.containsKey("curve-s4")) curveSpeed4 = constrain(doc["curve-s4"].as(), 0, 100); + if (doc.containsKey("curve-s5")) curveSpeed5 = constrain(doc["curve-s5"].as(), 0, 100); + + serializeConfig(); + updateFanSpeed(); + } + ); + webHandlerRegistered = true; - DEBUG_PRINTLN(F("GPU Fan: Web handler registered at /gpu-fan")); } public: void setup() override { initPWM(); - setFanPWM((fixedSpeedPct * 255) / 100); // Initial speed + setFanPWM((fixedSpeedPct * 255) / 100); lastTempUpdate = millis(); initDone = true; - - // Register web handler immediately during setup registerWebHandler(); - - DEBUG_PRINTLN(F("GPU Fan Controller initialized")); } void connected() override { - // Also try to register here in case setup was too early registerWebHandler(); } @@ -256,110 +203,75 @@ class GPUFanControllerUsermod : public Usermod { unsigned long now = millis(); - // Check for temperature timeout in curve mode if (controlMode == MODE_CURVE && (now - lastTempUpdate > tempTimeoutMs)) { - if (currentPWM != 255) { - setFanPWM(255); - DEBUG_PRINTLN(F("GPU Fan: Temperature timeout - running at 100% for safety")); - } + if (currentPWM != 255) setFanPWM(255); return; } - // Update fan speed periodically if (now - lastLoopTime > loopIntervalMs) { updateFanSpeed(); lastLoopTime = now; } } - // Handle HTTP API requests - bool handleButton(uint8_t b) override { - // Not used for this usermod - return false; - } + bool handleButton(uint8_t b) override { return false; } - // Add info to the WLED info panel void addToJsonInfo(JsonObject& root) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - // Add enable/disable button and link to curve editor JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString = F(""); - uiDomString += F(" [Editor]"); + uiDomString += F("\"> [Editor]"); infoArr.add(uiDomString); if (enabled) { - // Temperature display JsonArray tempArr = user.createNestedArray(F("GPU Temp")); tempArr.add(currentTemp); tempArr.add(F("°C")); - // Fan speed display JsonArray speedArr = user.createNestedArray(F("Fan Speed")); - uint8_t speedPct = (currentPWM * 100) / 255; - speedArr.add(speedPct); + speedArr.add((currentPWM * 100) / 255); speedArr.add(F("%")); - // Mode display JsonArray modeArr = user.createNestedArray(F("Fan Mode")); modeArr.add(controlMode == MODE_FIXED ? F("Fixed") : F("Curve")); - - // Manual speed slider - JsonArray sliderArr = user.createNestedArray(F("Manual")); - String sliderStr = F("
"); - sliderArr.add(sliderStr); } } - // Handle state changes from JSON API void readFromJsonState(JsonObject& root) override { if (!initDone) return; JsonObject usermod = root[FPSTR(_name)]; - if (!usermod.isNull()) { - // Enable/disable - if (usermod[FPSTR(_enabled)].is()) { - enabled = usermod[FPSTR(_enabled)].as(); - if (!enabled) setFanPWM(0); - } - - // Manual speed control - if (enabled && !usermod["speed"].isNull() && usermod["speed"].is()) { - fixedSpeedPct = usermod["speed"].as(); - controlMode = MODE_FIXED; // Switch to fixed mode when manually setting speed - updateFanSpeed(); - } - - // Mode selection - if (!usermod["mode"].isNull() && usermod["mode"].is()) { - controlMode = (ControlMode)usermod["mode"].as(); - } - - // Temperature update (from external source like Python script) - if (!usermod["temperature"].isNull()) { - currentTemp = usermod["temperature"].as(); - lastTempUpdate = millis(); - if (controlMode == MODE_CURVE) { - updateFanSpeed(); - } - } + if (usermod.isNull()) return; + + if (usermod["enabled"].is()) { + enabled = usermod["enabled"].as(); + if (!enabled) setFanPWM(0); + } + + if (usermod["speed"].is()) { + fixedSpeedPct = usermod["speed"].as(); + controlMode = MODE_FIXED; + updateFanSpeed(); + } + + if (usermod["mode"].is()) { + controlMode = (ControlMode)usermod["mode"].as(); + } + + if (!usermod["temperature"].isNull()) { + currentTemp = usermod["temperature"].as(); + lastTempUpdate = millis(); + if (controlMode == MODE_CURVE) updateFanSpeed(); } } - // Save configuration void addToConfig(JsonObject& root) override { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; @@ -368,8 +280,6 @@ class GPUFanControllerUsermod : public Usermod { top["fixed-speed"] = fixedSpeedPct; top["timeout-ms"] = (int)tempTimeoutMs; top["curve-points"] = curveCount; - - // Save curve points as flat values top["curve-t1"] = curveTemp1; top["curve-t2"] = curveTemp2; top["curve-t3"] = curveTemp3; @@ -382,86 +292,51 @@ class GPUFanControllerUsermod : public Usermod { top["curve-s5"] = curveSpeed5; } - // Append config data (labels for config page) void appendConfigData() override { - oappend(SET_F("addInfo('GPU-Fan:pwm-pin',1,'GPIO');")); - oappend(SET_F("addInfo('GPU-Fan:mode',1,'0=Fixed, 1=Curve');")); - oappend(SET_F("addInfo('GPU-Fan:fixed-speed',1,'%');")); - oappend(SET_F("addInfo('GPU-Fan:timeout-ms',1,'ms (safety timeout)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-points',1,'2-5 points');")); - oappend(SET_F("addInfo('GPU-Fan:curve-t1',1,'°C (Point 1 temp)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-s1',1,'% (Point 1 speed)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-t2',1,'°C (Point 2 temp)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-s2',1,'% (Point 2 speed)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-t3',1,'°C (Point 3 temp)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-s3',1,'% (Point 3 speed)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-t4',1,'°C (Point 4 temp)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-s4',1,'% (Point 4 speed)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-t5',1,'°C (Point 5 temp)');")); - oappend(SET_F("addInfo('GPU-Fan:curve-s5',1,'% (Point 5 speed)');")); + oappend(SET_F("addInfo('GPU-Fan:pwm-pin',1,'GPIO');")); + oappend(SET_F("addInfo('GPU-Fan:mode',1,'0=Fixed, 1=Curve');")); + oappend(SET_F("addInfo('GPU-Fan:fixed-speed',1,'%');")); } - // Load configuration bool readFromConfig(JsonObject& root) override { int8_t newPwmPin = pwmPin; - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINTLN(F("GPU Fan: No config found, using defaults")); - return false; - } + if (top.isNull()) return false; enabled = top[FPSTR(_enabled)] | enabled; newPwmPin = top["pwm-pin"] | newPwmPin; controlMode = (ControlMode)(top["mode"] | (int)controlMode); fixedSpeedPct = top["fixed-speed"] | fixedSpeedPct; tempTimeoutMs = top["timeout-ms"] | (int)tempTimeoutMs; - curveCount = top["curve-points"] | curveCount; - curveCount = constrain(curveCount, 2, MAX_CURVE_POINTS); + curveCount = constrain(top["curve-points"] | curveCount, 2, MAX_CURVE_POINTS); - // Load curve points curveTemp1 = top["curve-t1"] | curveTemp1; curveTemp2 = top["curve-t2"] | curveTemp2; curveTemp3 = top["curve-t3"] | curveTemp3; curveTemp4 = top["curve-t4"] | curveTemp4; curveTemp5 = top["curve-t5"] | curveTemp5; - curveSpeed1 = top["curve-s1"] | curveSpeed1; - curveSpeed2 = top["curve-s2"] | curveSpeed2; - curveSpeed3 = top["curve-s3"] | curveSpeed3; - curveSpeed4 = top["curve-s4"] | curveSpeed4; - curveSpeed5 = top["curve-s5"] | curveSpeed5; - - // Constrain speed values - curveSpeed1 = constrain(curveSpeed1, 0, 100); - curveSpeed2 = constrain(curveSpeed2, 0, 100); - curveSpeed3 = constrain(curveSpeed3, 0, 100); - curveSpeed4 = constrain(curveSpeed4, 0, 100); - curveSpeed5 = constrain(curveSpeed5, 0, 100); + curveSpeed1 = constrain(top["curve-s1"] | curveSpeed1, 0, 100); + curveSpeed2 = constrain(top["curve-s2"] | curveSpeed2, 0, 100); + curveSpeed3 = constrain(top["curve-s3"] | curveSpeed3, 0, 100); + curveSpeed4 = constrain(top["curve-s4"] | curveSpeed4, 0, 100); + curveSpeed5 = constrain(top["curve-s5"] | curveSpeed5, 0, 100); if (!initDone) { pwmPin = newPwmPin; - DEBUG_PRINTLN(F("GPU Fan: Config loaded")); - } else { - if (pwmPin != newPwmPin) { - DEBUG_PRINTLN(F("GPU Fan: Re-initializing pins")); - deinitPWM(); - pwmPin = newPwmPin; - setup(); - } + } else if (pwmPin != newPwmPin) { + deinitPWM(); + pwmPin = newPwmPin; + setup(); } - return !top["curve-t1"].isNull(); + return true; } - uint16_t getId() override { - return USERMOD_ID_UNSPECIFIED; // Change this if you add to const.h - } + uint16_t getId() override { return USERMOD_ID_UNSPECIFIED; } }; -// String constants const char GPUFanControllerUsermod::_name[] PROGMEM = "GPU-Fan"; const char GPUFanControllerUsermod::_enabled[] PROGMEM = "enabled"; -// Register the usermod static GPUFanControllerUsermod gpuFanController; REGISTER_USERMOD(gpuFanController);