Fix fan curve saving - use flat config structure for WLED compatibility

- Changed from nested JSON array to flat curve-t1/s1, curve-t2/s2, etc.
- Added appendConfigData() for config page labels
- Reduced max curve points to 5 for simpler config
- Added bounds checking for speed values
This commit is contained in:
dawie 2026-01-30 18:41:07 +02:00
parent 77a3853a4e
commit a3cd6bac16

View File

@ -10,7 +10,7 @@
* 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
* - Temperature curve mode with up to 5 configurable points
* - Safety fallback to 100% if temperature data times out
* - Integration with WLED's web interface
*
@ -54,19 +54,24 @@ class GPUFanControllerUsermod : public Usermod {
ControlMode controlMode = MODE_CURVE;
uint8_t fixedSpeedPct = 50; // 0-100%
// Temperature curve (up to 10 points)
static const uint8_t MAX_CURVE_POINTS = 10;
// 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;
struct CurvePoint {
float temp;
uint8_t speed;
};
CurvePoint curve[MAX_CURVE_POINTS] = {
{30.0f, 30},
{50.0f, 50},
{70.0f, 75},
{85.0f, 100}
};
// 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;
@ -132,28 +137,57 @@ class GPUFanControllerUsermod : public Usermod {
#endif
}
// Get curve temperature at index
int16_t getCurveTemp(uint8_t idx) {
switch(idx) {
case 0: return curveTemp1;
case 1: return curveTemp2;
case 2: return curveTemp3;
case 3: return curveTemp4;
case 4: return curveTemp5;
default: return 50;
}
}
// Get curve speed at index
uint8_t getCurveSpeed(uint8_t idx) {
switch(idx) {
case 0: return curveSpeed1;
case 1: return curveSpeed2;
case 2: return curveSpeed3;
case 3: return curveSpeed4;
case 4: return curveSpeed5;
default: return 50;
}
}
// 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;
if (temp <= getCurveTemp(0)) {
return getCurveSpeed(0);
}
// Above last point
if (temp >= curve[curveCount - 1].temp) {
return curve[curveCount - 1].speed;
if (temp >= getCurveTemp(curveCount - 1)) {
return getCurveSpeed(curveCount - 1);
}
// 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;
int16_t t1 = getCurveTemp(i);
int16_t t2 = getCurveTemp(i + 1);
uint8_t s1 = getCurveSpeed(i);
uint8_t s2 = getCurveSpeed(i + 1);
int speed = curve[i].speed + (int)((tempDiff / tempRange) * speedRange);
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);
}
}
@ -310,16 +344,39 @@ class GPUFanControllerUsermod : public Usermod {
top["pwm-pin"] = pwmPin;
top["mode"] = (int)controlMode;
top["fixed-speed"] = fixedSpeedPct;
top["timeout-ms"] = tempTimeoutMs;
top["timeout-ms"] = (int)tempTimeoutMs;
top["curve-points"] = curveCount;
// 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;
// Save curve points as flat values
top["curve-t1"] = curveTemp1;
top["curve-t2"] = curveTemp2;
top["curve-t3"] = curveTemp3;
top["curve-t4"] = curveTemp4;
top["curve-t5"] = curveTemp5;
top["curve-s1"] = curveSpeed1;
top["curve-s2"] = curveSpeed2;
top["curve-s3"] = curveSpeed3;
top["curve-s4"] = curveSpeed4;
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)');"));
}
// Load configuration
@ -336,23 +393,28 @@ class GPUFanControllerUsermod : public Usermod {
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;
tempTimeoutMs = top["timeout-ms"] | (int)tempTimeoutMs;
curveCount = top["curve-points"] | 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;
}
// 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);
if (!initDone) {
pwmPin = newPwmPin;
@ -366,7 +428,7 @@ class GPUFanControllerUsermod : public Usermod {
}
}
return !top["curve-count"].isNull();
return !top["curve-t1"].isNull();
}
uint16_t getId() override {