mirror of
https://github.com/compute-blade-community/compute-blade-agent.git
synced 2026-04-16 15:35:42 +02:00
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:
88
pkg/fancontroller/fancontroller.go
Normal file
88
pkg/fancontroller/fancontroller.go
Normal 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)
|
||||
}
|
||||
147
pkg/fancontroller/fancontroller_test.go
Normal file
147
pkg/fancontroller/fancontroller_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user