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:
parent
f19d29cd64
commit
8ff78b0413
383
usermods/GPU_Fan_Controller/GPU_Fan_Controller.cpp
Normal file
383
usermods/GPU_Fan_Controller/GPU_Fan_Controller.cpp
Normal 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("\"></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);
|
||||
Loading…
Reference in New Issue
Block a user