feat/fix: add linear fan speed control based on temperature

some smaller fixes

Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>
This commit is contained in:
Matthias Riegler
2023-09-04 19:59:33 +02:00
parent da9eea3320
commit 500a1a32d4
9 changed files with 388 additions and 38 deletions

View File

@@ -0,0 +1,88 @@
package fancontroller
import (
"fmt"
"sync"
)
type FanController interface {
Override(opts *FanOverrideOpts)
GetFanSpeed(temperature float64) uint8
}
type FanOverrideOpts struct {
Speed uint8
}
type FanControllerStep struct {
// Temperature is the temperature to react to
Temperature float64
// Speed is the fan speed in percent
Speed uint8
}
// FanController configures a fan controller for the computeblade
type FanControllerConfig struct {
// Steps defines the temperature/speed steps for the fan controller
Steps []FanControllerStep
}
// FanController is a simple fan controller that reacts to temperature changes with a linear function
type fanControllerLinear struct {
mu sync.Mutex
overrideOpts *FanOverrideOpts
config FanControllerConfig
}
// NewFanControllerLinear creates a new FanControllerLinear
func NewLinearFanController(config FanControllerConfig) (FanController, error) {
// Validate config for a very simple linear fan controller
if len(config.Steps) != 2 {
return nil, fmt.Errorf("exactly two steps must be defined")
}
if config.Steps[0].Temperature > config.Steps[1].Temperature {
return nil, fmt.Errorf("step 1 temperature must be lower than step 2 temperature")
}
if config.Steps[0].Speed > config.Steps[1].Speed {
return nil, fmt.Errorf("step 1 speed must be lower than step 2 speed")
}
if config.Steps[0].Speed > 100 || config.Steps[1].Speed > 100 {
return nil, fmt.Errorf("speed must be between 0 and 100")
}
return &fanControllerLinear{
config: config,
}, nil
}
func (f *fanControllerLinear) Override(opts *FanOverrideOpts) {
f.mu.Lock()
defer f.mu.Unlock()
f.overrideOpts = opts
}
// GetFanSpeed returns the fan speed in percent based on the current temperature
func (f *fanControllerLinear) GetFanSpeed(temperature float64) uint8 {
f.mu.Lock()
defer f.mu.Unlock()
if f.overrideOpts != nil {
return f.overrideOpts.Speed
}
if temperature <= f.config.Steps[0].Temperature {
return f.config.Steps[0].Speed
}
if temperature >= f.config.Steps[1].Temperature {
return f.config.Steps[1].Speed
}
// Calculate slope
slope := float64(f.config.Steps[1].Speed-f.config.Steps[0].Speed) / (f.config.Steps[1].Temperature - f.config.Steps[0].Temperature)
// Calculate speed
speed := float64(f.config.Steps[0].Speed) + slope*(temperature-f.config.Steps[0].Temperature)
return uint8(speed)
}

View File

@@ -0,0 +1,147 @@
// fancontroller_test.go
package fancontroller_test
import (
"testing"
"github.com/xvzf/computeblade-agent/pkg/fancontroller"
)
func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
t.Parallel()
config := fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 20, Speed: 30},
{Temperature: 30, Speed: 60},
},
}
controller, err := fancontroller.NewLinearFanController(config)
if err != nil {
t.Fatalf("Failed to create fan controller: %v", err)
}
testCases := []struct {
temperature float64
expected uint8
}{
{15, 30}, // Should use the minimum speed
{25, 45}, // Should calculate speed based on linear function
{35, 60}, // Should use the maximum speed
}
for _, tc := range testCases {
expected := tc.expected
temperature := tc.temperature
t.Run("", func(t *testing.T) {
t.Parallel()
speed := controller.GetFanSpeed(temperature)
if speed != expected {
t.Errorf("For temperature %.2f, expected speed %d but got %d", temperature, expected, speed)
}
})
}
}
func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
t.Parallel()
config := fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 20, Speed: 30},
{Temperature: 30, Speed: 60},
},
}
controller, err := fancontroller.NewLinearFanController(config)
if err != nil {
t.Fatalf("Failed to create fan controller: %v", err)
}
controller.Override(&fancontroller.FanOverrideOpts{
Speed: 99,
})
testCases := []struct {
temperature float64
expected uint8
}{
{15, 99},
{25, 99},
{35, 99},
}
for _, tc := range testCases {
expected := tc.expected
temperature := tc.temperature
t.Run("", func(t *testing.T) {
t.Parallel()
speed := controller.GetFanSpeed(temperature)
if speed != expected {
t.Errorf("For temperature %.2f, expected speed %d but got %d", temperature, expected, speed)
}
})
}
}
func TestFanControllerLinear_ConstructionErrors(t *testing.T) {
testCases := []struct {
name string
config fancontroller.FanControllerConfig
errMsg string
}{
{
name: "InvalidStepCount",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 20, Speed: 30},
},
},
errMsg: "exactly two steps must be defined",
},
{
name: "InvalidStepTemperatures",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 30, Speed: 60},
{Temperature: 20, Speed: 30},
},
},
errMsg: "step 1 temperature must be lower than step 2 temperature",
},
{
name: "InvalidStepSpeeds",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 20, Speed: 60},
{Temperature: 30, Speed: 30},
},
},
errMsg: "step 1 speed must be lower than step 2 speed",
},
{
name: "InvalidSpeedRange",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 20, Speed: 10},
{Temperature: 30, Speed: 200},
},
},
errMsg: "speed must be between 0 and 100",
},
}
for _, tc := range testCases {
config := tc.config
expectedErrMsg := tc.errMsg
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := fancontroller.NewLinearFanController(config)
if err == nil {
t.Errorf("Expected error with message '%s', but got no error", expectedErrMsg)
} else if err.Error() != expectedErrMsg {
t.Errorf("Expected error message '%s', but got '%s'", expectedErrMsg, err.Error())
}
})
}
}

View File

@@ -22,6 +22,11 @@ var (
Name: "fan_speed",
Help: "Fan speed in RPM",
})
socTemperature = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "computeblade",
Name: "soc_temperature",
Help: "SoC temperature in °C",
})
computeModule = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "computeblade",
Name: "compute_modul_present",
@@ -103,6 +108,8 @@ type ComputeBladeHal interface {
SetLed(idx uint, color LedColor) error
// GetPowerStatus returns the current power status of the blade
GetPowerStatus() (PowerStatus, error)
// GetTemperature returns the current temperature of the SoC in °C
GetTemperature() (float64, error)
// GetEdgeButtonPressChan returns a channel emitting edge button press events
WaitForEdgeButtonPress(ctx context.Context) error
}

View File

@@ -6,7 +6,10 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -47,6 +50,8 @@ const (
bcm283xRegPwmclkCntrlBitEnable = 4
bcm283xDebounceInterval = 100 * time.Millisecond
bcm283xThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
)
type bcm283x struct {
@@ -114,17 +119,17 @@ func NewCm4Hal(opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
}
bcm := &bcm283x{
devmem: devmem,
gpioMem: gpioMem,
gpioMem8: gpioMem8,
pwmMem: pwmMem,
pwmMem8: pwmMem8,
clkMem: clkMem,
clkMem8: clkMem8,
gpioChip0: gpioChip0,
opts: opts,
devmem: devmem,
gpioMem: gpioMem,
gpioMem8: gpioMem8,
pwmMem: pwmMem,
pwmMem8: pwmMem8,
clkMem: clkMem,
clkMem8: clkMem8,
gpioChip0: gpioChip0,
opts: opts,
edgeButtonDebounceChan: make(chan struct{}, 1),
edgeButtonWatchChan: make(chan struct{}),
edgeButtonWatchChan: make(chan struct{}),
}
computeModule.WithLabelValues("cm4").Set(1)
@@ -181,7 +186,7 @@ func (bcm *bcm283x) handleEdgeButtonEdge(evt gpiod.LineEvent) {
case bcm.edgeButtonDebounceChan <- struct{}{}:
go func() {
// Manually debounce the button
defer <- bcm.edgeButtonDebounceChan
<-bcm.edgeButtonDebounceChan
time.Sleep(bcm283xDebounceInterval)
edgeButtonEventCount.Inc()
close(bcm.edgeButtonWatchChan)
@@ -440,3 +445,27 @@ func (bcm *bcm283x) updateLEDs() error {
return nil
}
// GetTemperature returns the current temperature of the SoC
func (bcm *bcm283x) GetTemperature() (float64, error) {
// Read temperature
f, err := os.Open(bcm283xThermalZonePath)
if err != nil {
return -1, err
}
raw, err := io.ReadAll(f)
if err != nil {
return -1, err
}
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
if err != nil {
return -1, err
}
temp := float64(cpuTemp) / 1000.0
socTemperature.Set(temp)
return temp, nil
}

View File

@@ -76,3 +76,8 @@ func (m *SimulatedHal) SetLed(idx uint, color LedColor) error {
m.logger.Info("SetLed", zap.Uint("idx", idx), zap.Any("color", color))
return nil
}
func (m *SimulatedHal) GetTemperature() (float64, error) {
m.logger.Info("GetTemperature")
return 42, nil
}

View File

@@ -48,3 +48,8 @@ func (m *ComputeBladeHalMock) SetLed(idx uint, color LedColor) error {
args := m.Called(idx, color)
return args.Error(0)
}
func (m *ComputeBladeHalMock) GetTemperature() (float64, error) {
args := m.Called()
return args.Get(0).(float64), args.Error(1)
}