Add GPU Fan Controller usermod C++ file

- PWM fan control for GPU cooling
- Fixed speed and temperature curve modes
- HTTP API for temperature updates
- WLED UI integration
This commit is contained in:
dawie 2026-01-30 17:35:59 +02:00
parent f19d29cd64
commit 8ff78b0413

View File

@ -0,0 +1,383 @@
#include "wled.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 10 configurable points
* - 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
*/
#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;
int8_t pwmPin = PWM_FAN_PIN;
#ifdef ARDUINO_ARCH_ESP32
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%
// Temperature curve (up to 10 points)
static const uint8_t MAX_CURVE_POINTS = 10;
uint8_t curveCount = 4;
struct CurvePoint {
float temp;
uint8_t speed;
};
CurvePoint curve[MAX_CURVE_POINTS] = {
{30.0f, 30},
{50.0f, 50},
{70.0f, 75},
{85.0f, 100}
};
// Runtime state
float currentTemp = 25.0f;
uint8_t currentPWM = 0;
unsigned long lastTempUpdate = 0;
unsigned long tempTimeoutMs = 10000; // 10 second timeout
unsigned long lastLoopTime = 0;
unsigned long loopIntervalMs = 2000; // Update every 2 seconds
// 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;
}
#ifdef ESP8266
analogWriteRange(255);
analogWriteFreq(GPU_FAN_PWM_FREQ);
#else
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) {
PinManager::deallocateLedc(pwmChannel, 1);
pwmChannel = 255;
}
#endif
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);
#else
ledcWrite(pwmChannel, pwmValue);
#endif
}
// Calculate fan speed from temperature curve
uint8_t calculateCurveSpeed(float temp) {
if (curveCount < 2) return 50; // Fallback
// Below first point
if (temp <= curve[0].temp) {
return curve[0].speed;
}
// Above last point
if (temp >= curve[curveCount - 1].temp) {
return curve[curveCount - 1].speed;
}
// Linear interpolation between points
for (uint8_t i = 0; i < curveCount - 1; i++) {
if (temp >= curve[i].temp && temp <= curve[i + 1].temp) {
float tempRange = curve[i + 1].temp - curve[i].temp;
float speedRange = curve[i + 1].speed - curve[i].speed;
float tempDiff = temp - curve[i].temp;
int speed = curve[i].speed + (int)((tempDiff / tempRange) * speedRange);
return constrain(speed, 0, 100);
}
}
return 50; // Fallback
}
// 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 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");
}
}
public:
void setup() override {
initPWM();
setFanPWM((fixedSpeedPct * 255) / 100); // Initial speed
lastTempUpdate = millis();
initDone = true;
DEBUG_PRINTLN(F("GPU Fan Controller initialized"));
}
void connected() override {
// Nothing special needed on WiFi connect
}
void loop() override {
if (!enabled || strip.isUpdating()) return;
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"));
}
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;
}
// 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
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("\">&#xe08f;</i></button>");
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(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("<div class=\"slider\"><div class=\"sliderwrap il\"><input class=\"noslide\" onchange=\"requestJson({'");
sliderStr += FPSTR(_name);
sliderStr += F("':{'speed':parseInt(this.value)}});\" oninput=\"updateTrail(this);\" max=100 min=0 type=\"range\" value=");
sliderStr += String(fixedSpeedPct);
sliderStr += F(" /><div class=\"sliderdisplay\"></div></div></div>");
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<bool>()) {
enabled = usermod[FPSTR(_enabled)].as<bool>();
if (!enabled) setFanPWM(0);
}
// Manual speed control
if (enabled && !usermod["speed"].isNull() && usermod["speed"].is<int>()) {
fixedSpeedPct = usermod["speed"].as<int>();
controlMode = MODE_FIXED; // Switch to fixed mode when manually setting speed
updateFanSpeed();
}
// Mode selection
if (!usermod["mode"].isNull() && usermod["mode"].is<int>()) {
controlMode = (ControlMode)usermod["mode"].as<int>();
}
// Temperature update (from external source like Python script)
if (!usermod["temperature"].isNull() && usermod["temperature"].is<float>()) {
currentTemp = usermod["temperature"].as<float>();
lastTempUpdate = millis();
if (controlMode == MODE_CURVE) {
updateFanSpeed();
}
}
}
}
// Save configuration
void addToConfig(JsonObject& root) override {
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
top["pwm-pin"] = pwmPin;
top["mode"] = (int)controlMode;
top["fixed-speed"] = fixedSpeedPct;
top["timeout-ms"] = tempTimeoutMs;
// Save curve points
top["curve-count"] = curveCount;
JsonArray curveArr = top.createNestedArray("curve");
for (uint8_t i = 0; i < curveCount; i++) {
JsonObject point = curveArr.createNestedObject();
point["temp"] = curve[i].temp;
point["speed"] = curve[i].speed;
}
}
// 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;
}
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"] | tempTimeoutMs;
// Load curve points
curveCount = top["curve-count"] | curveCount;
curveCount = constrain(curveCount, 2, MAX_CURVE_POINTS);
JsonArray curveArr = top["curve"];
if (!curveArr.isNull()) {
uint8_t i = 0;
for (JsonObject point : curveArr) {
if (i >= MAX_CURVE_POINTS) break;
curve[i].temp = point["temp"] | curve[i].temp;
curve[i].speed = point["speed"] | curve[i].speed;
i++;
}
curveCount = i;
}
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();
}
}
return !top["curve-count"].isNull();
}
uint16_t getId() override {
return USERMOD_ID_UNSPECIFIED; // Change this if you add to const.h
}
};
// String constants
const char GPUFanControllerUsermod::_name[] PROGMEM = "GPU-Fan";
const char GPUFanControllerUsermod::_enabled[] PROGMEM = "enabled";
// Register the usermod
static GPUFanControllerUsermod gpuFanController;
REGISTER_USERMOD(gpuFanController);