Files
compute-blade-agent/pkg/fancontroller/fancontroller.go
weslson 84346089ca fix(fancontroller): support more than 2 steps in fan curve (#156)
* fix(fancontroller): support more than 2 steps in fan curve

The GetFanSpeedPercent function only used the first 2 steps from the
config, ignoring all subsequent steps. This caused the fan to cap at
step[1]'s percent value regardless of actual temperature.

For example, with a typical 5-step curve:
  - 40°C → 30%
  - 50°C → 50%
  - 60°C → 70%
  - 70°C → 90%
  - 75°C → 100%

Any temperature ≥50°C would return 50% instead of the correct value.
At 77°C the fan should be at 100%, but was stuck at 50%.

The fix properly iterates through all configured steps to find the
correct temperature bracket and interpolates within it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Apply suggestions from code review

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:26:29 +01:00

119 lines
3.5 KiB
Go

package fancontroller
import (
"fmt"
"sort"
"sync"
"github.com/sierrasoftworks/humane-errors-go"
)
type FanController interface {
Override(opts *FanOverrideOpts)
// GetFanSpeedPercent returns the fan speed in percent based on the current temperature
GetFanSpeedPercent(temperature float64) uint8
// IsAutomaticSpeed returns true if the FanSpeed is determined by the fan controller logic, or false if determined
// by an FanOverrideOpts
IsAutomaticSpeed() bool
// Steps returns the list of temperature and fan speed steps configured for the fan controller.
Steps() []Step
}
// FanController is a simple fan controller that reacts to temperature changes with a linear function
type fanControllerLinear struct {
mu sync.Mutex
overrideOpts *FanOverrideOpts
config Config
}
// NewLinearFanController creates a new FanControllerLinear
func NewLinearFanController(config Config) (FanController, humane.Error) {
steps := config.Steps
// Sort steps by temperature
sort.Slice(steps, func(i, j int) bool {
return steps[i].Temperature < steps[j].Temperature
})
for i := 0; i < len(steps)-1; i++ {
curr := steps[i]
next := steps[i+1]
if curr.Temperature >= next.Temperature {
return nil, humane.New("steps must have strictly increasing temperatures",
"Ensure that the temperatures are in ascending order and the ranges do not overlap",
fmt.Sprintf("Ensure defined temperature stepd %.2f is >= %.2f", curr.Temperature, next.Temperature),
)
}
if curr.Percent > next.Percent {
return nil, humane.New("fan percent must not decrease",
"Ensure that the fan percentages are not decreasing for higher temperatures",
fmt.Sprintf("Temperature %.2f is defined at %d%% and must be >= %d%% defined for temperature %.2f", curr.Temperature, curr.Percent, next.Percent, next.Temperature),
)
}
}
for _, step := range steps {
if step.Percent > 100 {
return nil, humane.New("fan percent must be between 0 and 100",
fmt.Sprintf("Ensure your fan percentage is 0 < %d < 100", step.Percent),
)
}
}
return &fanControllerLinear{
config: config,
}, nil
}
func (f *fanControllerLinear) Steps() []Step {
return f.config.Steps
}
func (f *fanControllerLinear) Override(opts *FanOverrideOpts) {
f.mu.Lock()
defer f.mu.Unlock()
f.overrideOpts = opts
}
// GetFanSpeedPercent returns the fan speed in percent based on the current temperature
func (f *fanControllerLinear) GetFanSpeedPercent(temperature float64) uint8 {
f.mu.Lock()
defer f.mu.Unlock()
if f.overrideOpts != nil {
return f.overrideOpts.Percent
}
steps := f.config.Steps
// Below minimum temperature: use minimum fan speed
if temperature <= steps[0].Temperature {
return steps[0].Percent
}
// Above maximum temperature: use maximum fan speed
lastIdx := len(steps) - 1
if temperature >= steps[lastIdx].Temperature {
return steps[lastIdx].Percent
}
// Find the bracket where steps[i].Temperature <= temperature < steps[i+1].Temperature
for i := 0; i < lastIdx; i++ {
if temperature >= steps[i].Temperature && temperature < steps[i+1].Temperature {
// Linear interpolation between steps[i] and steps[i+1]
slope := float64(steps[i+1].Percent-steps[i].Percent) / (steps[i+1].Temperature - steps[i].Temperature)
speed := float64(steps[i].Percent) + slope*(temperature-steps[i].Temperature)
return uint8(speed)
}
}
// Fallback (should not reach here due to above checks)
return steps[lastIdx].Percent
}
func (f *fanControllerLinear) IsAutomaticSpeed() bool {
return f.overrideOpts == nil
}