Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Fix/linear pwm adjustment #267

Merged
merged 34 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
04eea1a
don't use PID control logic, but linearly approaching the desired PWM
jwefers Nov 27, 2023
022531f
don't try to scale target pwm into a pwm window - needs work, if stop…
jwefers Nov 27, 2023
7b44b5e
add debug line for curve calculation desired pwm value
jwefers Nov 27, 2023
6a8380a
completely untested unfinished work towards linear control alg as opt…
jwefers Feb 27, 2024
dafe42c
Merge branch 'master' into fix/linearPwmAdjustment
markusressel Sep 23, 2024
12926eb
rename config parameter "PwmChangePerSecond" -> "MaxPwmChangePerCycle"
markusressel Sep 23, 2024
f889c9b
rework config structure, add support for parsing strings as well as s…
markusressel Sep 23, 2024
020acd3
revert pid loop to previous implementation
markusressel Sep 23, 2024
c59c80e
refactoring and cleanup
markusressel Sep 23, 2024
76938dc
improve error handling, make maxPwmChangePerCycle optional
markusressel Sep 23, 2024
0741d4f
lint fix
markusressel Sep 23, 2024
98b295d
cleanup
markusressel Sep 23, 2024
cdfd290
added validation, deprecate ControlLoop
markusressel Sep 23, 2024
f9a6c9f
fix config parsing, make maxPwmChangePerCycle optional, update docume…
markusressel Sep 23, 2024
9b9d616
fix crash
markusressel Sep 23, 2024
9cb1cfd
fix lint
markusressel Sep 23, 2024
ac84fff
fix lint
markusressel Sep 23, 2024
0264c5e
use maxPwmChangePerCycle default value of 10
markusressel Sep 23, 2024
3b07b17
actually, dont use maxPwmChangePerCycle default value, since that wou…
markusressel Sep 23, 2024
e440807
use PID controller by default
markusressel Sep 23, 2024
087d1e7
refactor
markusressel Sep 23, 2024
d035787
fix default PID controller values, update README.md
markusressel Sep 23, 2024
2e90287
refactor
markusressel Sep 23, 2024
91935f0
update README.md
markusressel Sep 24, 2024
2e80fc3
concurrency fixes
markusressel Sep 25, 2024
74bb213
fix tests
markusressel Sep 25, 2024
c8c77fb
refactor global access to SpeedCurves, Fans and Sensors
markusressel Sep 25, 2024
16e5594
refactor, update dependencies, error handling
markusressel Sep 25, 2024
f254ee7
update go version
markusressel Sep 25, 2024
63976fb
update golang-lint
markusressel Sep 25, 2024
8c6ae73
lint fixes
markusressel Sep 25, 2024
7b06d4d
lint fixes
markusressel Sep 25, 2024
d1934cf
print version
markusressel Sep 25, 2024
de1d870
0.9.0
markusressel Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions fan2go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ fans:
# The curve ID (defined above) that should be used to determine the
# speed of this fan
curve: cpu_curve
# (Optional) The algorithm how the target speed, determined by the curve is approached.
# default: pid: uses the PID control algorithm with default tuning variables
# simple: together with pwmChangePerSecond, fan speeds will approach target value
# with the given max speed. Default is 10
controlAlgorithm:
alg: simple
markusressel marked this conversation as resolved.
Show resolved Hide resolved
pwmChangePerSecond: 10
# (Optional) Override for the lowest PWM value at which the
# fan is able to maintain rotation if it was spinning previously.
minPwm: 30
Expand Down
35 changes: 21 additions & 14 deletions internal/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,21 +228,28 @@ func initializeObjects(pers persistence.Persistence) map[fans.Fan]controller.Fan
for config, fan := range initializeFans(controllers) {
updateRate := configuration.CurrentConfig.ControllerAdjustmentTickRate

var pidLoop util.PidLoop
if config.ControlLoop != nil {
markusressel marked this conversation as resolved.
Show resolved Hide resolved
pidLoop = *util.NewPidLoop(
config.ControlLoop.P,
config.ControlLoop.I,
config.ControlLoop.D,
)
} else {
pidLoop = *util.NewPidLoop(
0.03,
0.002,
0.0005,
)
var pidLoop *util.PidLoop
var linearLoop *util.LinearLoop
if config.ControlAlgorithm.Alg == "pid" {

if config.ControlLoop != nil {
pidLoop = util.NewPidLoop(
config.ControlLoop.P,
config.ControlLoop.I,
config.ControlLoop.D,
)
} else {
pidLoop = util.NewPidLoop(
0.03,
0.002,
0.0005,
)
}
} else if config.ControlAlgorithm.Alg == "simple" {
linearLoop = util.NewLinearLoop(config.ControlAlgorithm.PwmChangePerSecond)
}
fanController := controller.NewFanController(pers, fan, pidLoop, updateRate)

fanController := controller.NewFanController(pers, fan, pidLoop, linearLoop, updateRate)
result[fan] = fanController
}

Expand Down
6 changes: 5 additions & 1 deletion internal/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ func setDefaultValues() {
viper.SetDefault("ControllerAdjustmentTickRate", 200*time.Millisecond)

viper.SetDefault("sensors", []SensorConfig{})
viper.SetDefault("fans", []FanConfig{})
viper.SetDefault("fans", []FanConfig{
// the control algorithm is a string enum, so we have to set an explicit default,
// the rest are primitive types with a well-defined default value
{ControlAlgorithm: ControlAlgorithmConfig{Alg: "pid"}},
})
}

// DetectAndReadConfigFile detects the path of the first existing config file
Expand Down
27 changes: 20 additions & 7 deletions internal/configuration/fans.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@ type FanConfig struct {
// StartPwm defines the lowest PWM value where the fans are able to start spinning from a standstill
StartPwm *int `json:"startPwm,omitempty"`
// MaxPwm defines the highest PWM value that yields an RPM increase
MaxPwm *int `json:"maxPwm,omitempty"`
PwmMap *map[int]int `json:"pwmMap,omitempty"`
Curve string `json:"curve"`
HwMon *HwMonFanConfig `json:"hwMon,omitempty"`
File *FileFanConfig `json:"file,omitempty"`
Cmd *CmdFanConfig `json:"cmd,omitempty"`
ControlLoop *ControlLoopConfig `json:"controlLoop,omitempty"`
MaxPwm *int `json:"maxPwm,omitempty"`
PwmMap *map[int]int `json:"pwmMap,omitempty"`
Curve string `json:"curve"`
ControlAlgorithm ControlAlgorithmConfig `json:"controlAlgorithm,omitempty"`
HwMon *HwMonFanConfig `json:"hwMon,omitempty"`
File *FileFanConfig `json:"file,omitempty"`
Cmd *CmdFanConfig `json:"cmd,omitempty"`
ControlLoop *ControlLoopConfig `json:"controlLoop,omitempty"`
markusressel marked this conversation as resolved.
Show resolved Hide resolved
}

type ControlAlgorithm string

const (
pid ControlAlgorithm = "pid"
simple ControlAlgorithm = "simple"
)

type ControlAlgorithmConfig struct {
Alg ControlAlgorithm `json:"alg,omitempty"`
PwmChangePerSecond int `json:"pwmChangePerSecond,omitempty"`
}

type HwMonFanConfig struct {
Expand Down
46 changes: 33 additions & 13 deletions internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type PidFanController struct {
pwmMap map[int]int
// PID loop for the PWM control
pidLoop *util.PidLoop
// alternatively: linear loop for PWM control
linearLoop *util.LinearLoop

// offset applied to the actual minPwm of the fan to ensure "neverStops" constraint
minPwmOffset int
Expand All @@ -73,7 +75,8 @@ type PidFanController struct {
func NewFanController(
persistence persistence.Persistence,
fan fans.Fan,
pidLoop util.PidLoop,
pidLoop *util.PidLoop,
linearLoop *util.LinearLoop,
updateRate time.Duration,
) FanController {
return &PidFanController{
Expand All @@ -83,7 +86,8 @@ func NewFanController(
updateRate: updateRate,
pwmValuesWithDistinctTarget: []int{},
pwmMap: map[int]int{},
pidLoop: &pidLoop,
pidLoop: pidLoop,
linearLoop: linearLoop,
minPwmOffset: 0,
}
}
Expand Down Expand Up @@ -239,20 +243,35 @@ func (f *PidFanController) UpdateFanSpeed() error {
// calculate the direct optimal target speed
target := f.calculateTargetPwm()

// ask the PID controller how to proceed
pidChange := math.Ceil(f.pidLoop.Loop(float64(target), float64(lastSetPwm)))
// the target pwm, approaching the actual target smoothly
var stepTarget int

// the last value set on the pid controller target
pidControllerTarget := math.Ceil(f.pidLoop.Loop(float64(target), float64(lastSetPwm)))
pidControllerTarget = pidControllerTarget + pidChange
// default algorithm: use a pid controller alg to approach target
if fan.GetControlAlgorithm().Alg == "pid" {
// ask the PID controller how to proceed
pidChange := math.Ceil(f.pidLoop.Loop(float64(target), float64(lastSetPwm)))

// ensure we are within sane bounds
coerced := util.Coerce(float64(lastSetPwm)+pidControllerTarget, 0, 255)
roundedTarget := int(math.Round(coerced))
// the last value set on the pid controller target
pidControllerTarget := math.Ceil(f.pidLoop.Loop(float64(target), float64(lastSetPwm)))
pidControllerTarget = pidControllerTarget + pidChange

// ensure we are within sane bounds
coerced := util.Coerce(float64(lastSetPwm)+pidControllerTarget, 0, 255)
stepTarget = int(math.Round(coerced))

// optional simple value
} else if fan.GetControlAlgorithm().Alg == "simple" {
// ask the PID controller how to proceed
pwmChange := math.Ceil(f.pidLoop.Loop(float64(target), float64(lastSetPwm)))

// ensure we are within sane bounds
coerced := util.Coerce(float64(lastSetPwm)+pwmChange, 0, 255)
stepTarget = int(math.Round(coerced))
}

if target >= 0 {
_ = trySetManualPwm(f.fan)
err := f.setPwm(roundedTarget)
err := f.setPwm(stepTarget)
if err != nil {
ui.Error("Error setting %s: %v", fan.GetId(), err)
}
Expand Down Expand Up @@ -420,10 +439,11 @@ func (f *PidFanController) calculateTargetPwm() int {

// map the target value to the possible range of this fan
maxPwm := fan.GetMaxPwm()
minPwm := fan.GetMinPwm() + f.minPwmOffset
// minPwm := fan.GetMinPwm() + f.minPwmOffset
markusressel marked this conversation as resolved.
Show resolved Hide resolved

// TODO: this assumes a linear curve, but it might be something else
target = minPwm + int((float64(target)/fans.MaxPwmValue)*(float64(maxPwm)-float64(minPwm)))
// TODO: remove
// target = minPwm + int((float64(target)/fans.MaxPwmValue)*(float64(maxPwm)-float64(minPwm)))

if f.lastSetPwm != nil && f.pwmMap != nil {
lastSetPwm := *(f.lastSetPwm)
Expand Down
5 changes: 4 additions & 1 deletion internal/curves/linear.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package curves

import (
"math"

"github.com/markusressel/fan2go/internal/configuration"
"github.com/markusressel/fan2go/internal/sensors"
"github.com/markusressel/fan2go/internal/ui"
"github.com/markusressel/fan2go/internal/util"
"math"
)

type LinearSpeedCurve struct {
Expand Down Expand Up @@ -39,6 +41,7 @@ func (c *LinearSpeedCurve) Evaluate() (value int, err error) {
}
}

ui.Debug("Evaluating curve '%s'. Sensor '%s' temp '%.0f°'. Desired PWM: %d", c.Config.ID, sensor.GetId(), sensor.GetMovingAvg()/1000, value)
c.Value = value
return value, nil
}
5 changes: 4 additions & 1 deletion internal/fans/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package fans

import (
"fmt"
"github.com/markusressel/fan2go/internal/configuration"
"sort"

"github.com/markusressel/fan2go/internal/configuration"
)

const (
Expand Down Expand Up @@ -64,6 +65,8 @@ type Fan interface {
// GetCurveId returns the id of the speed curve associated with this fan
GetCurveId() string

GetControlAlgorithm() configuration.ControlAlgorithmConfig

// ShouldNeverStop indicated whether this fan should never stop rotating
ShouldNeverStop() bool

Expand Down
4 changes: 4 additions & 0 deletions internal/fans/hwmon.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ func (fan *HwMonFan) GetCurveId() string {
return fan.Config.Curve
}

func (fan *HwMonFan) GetControlAlgorithm() configuration.ControlAlgorithmConfig {
return fan.Config.ControlAlgorithm
}

func (fan *HwMonFan) ShouldNeverStop() bool {
return fan.Config.NeverStop
}
Expand Down
42 changes: 42 additions & 0 deletions internal/util/linear_control.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package util

import "time"

// an alternative loop control to gracefully
// approach the target in fan speed in a linear fashion by simply
// changing the pwm speed at most by x amount per cycle

type LinearLoop struct {
pwmChangePerSecond int
lastTime time.Time
}

func NewLinearLoop(pwmChangePerSecond int) *LinearLoop {
return &LinearLoop{
pwmChangePerSecond: pwmChangePerSecond,
lastTime: time.Now(),
}
}

func (l *LinearLoop) Loop(target float64, measured float64) float64 {
loopTime := time.Now()

dt := loopTime.Sub(l.lastTime).Seconds()

l.lastTime = loopTime

// the pwm adjustment depends on the direction and
// the time-based change speed limit.
maxPwmChangeThiStep := float64(l.pwmChangePerSecond) * dt
err := target - measured
// we can be above or below the target pwm value,
// so we substract or add at most the max pwm change,
// capped to having reached the target
if err > 0 {
// below desired speed, add pwms
return Coerce(maxPwmChangeThiStep, 0, err)
} else {
// above or at desired speed, subtract pwms
return Coerce(-maxPwmChangeThiStep, err, 0)
}
}
32 changes: 19 additions & 13 deletions internal/util/pid.go
markusressel marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package util

import "time"
import (
"time"
)

type PidLoop struct {
// Proptional Constant
Expand Down Expand Up @@ -30,23 +32,27 @@ func NewPidLoop(p float64, i float64, d float64) *PidLoop {

// Loop advances the pid loop
func (p *PidLoop) Loop(target float64, measured float64) float64 {
output := 0.0
err := target - measured
// TODO: make user configurable
const maxPwmAdjustmentPerSecond float64 = 10.0

loopTime := time.Now()
var dt float64
if p.lastTime.IsZero() {
p.lastTime = loopTime
dt = 1
} else {
dt := loopTime.Sub(p.lastTime).Seconds()

proportional := err
p.integral = p.integral + err*dt
derivative := (err - p.error) / dt
output = p.p*proportional + p.i*p.integral + p.d*derivative
dt = loopTime.Sub(p.lastTime).Seconds()
}

p.error = err
p.lastTime = loopTime

return output
// the pwm adjustment depends on the direction and
// the time-based change speed limit.
maxPwmAdjustmentThiStep := maxPwmAdjustmentPerSecond * dt
err := target - measured
if err > 0 {
return Coerce(maxPwmAdjustmentThiStep, 0, err)
} else if err < 0 {
return Coerce(-maxPwmAdjustmentThiStep, err, 0)
} else {
return 0
}
}