From f9a6c9f6b989c90f8c773fe1b30d1ac5e1fb9893 Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Mon, 23 Sep 2024 08:29:16 +0200 Subject: [PATCH] fix config parsing, make maxPwmChangePerCycle optional, update documentation --- README.md | 49 +++++++++++++++++++++++++--- cmd/fan/init.go | 3 +- fan2go.yaml | 3 +- internal/backend.go | 2 +- internal/configuration/config.go | 22 +++++++++++-- internal/configuration/fans.go | 2 +- internal/configuration/validation.go | 3 +- 7 files changed, 71 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7851f59..0c4a7d0 100644 --- a/README.md +++ b/README.md @@ -654,7 +654,45 @@ sensor value. ## Fan Controllers -Fan speed is controlled by a PID controller per each configured fan. The default +The speed of a Fan is controlled using a combination of its curve, a control algorithm and the properties of +the fan controller itself. + +The curve is used as the target value for the control algorithm to reach. The control algorithm then calculates the +next PWM value to apply to the fan to reach this target value. The fan controller then applies this PWM value to the +fan, while respecting constraints like the minimum and maximum PWM values, as well as the `neverStop` flag. + +### Control Algorithms + +A control algorithm +is a function that returns the next PWM value to apply based on the target value calculated by the curve. The simplest +control algorithm is the direct control algorithm, which simply forwards the target value to the fan. + +#### Direct Control Algorithm + +The simplest control algorithm is the direct control algorithm. It simply forwards the curve value to the fan +controller. + +```yaml +fans: + - id: some_fan + ... + controlAlgorithm: direct +``` + +This control algorithm can also be used to approach the curve value more slowly: + +```yaml +fans: + - id: some_fan + ... + controlAlgorithm: + direct: + maxPwmChangePerCycle: 10 +``` + +### PID Control Algorithm + +The PID control algorithm uses a PID loop to approach the target value. The default configuration is pretty non-aggressive using the following values: | P | I | D | @@ -667,10 +705,11 @@ If you don't like the default behaviour you can configure your own in the config fans: - id: some_fan ... - controlLoop: - p: 0.03 - i: 0.002 - d: 0.0005 + controlAlgorithm: + pid: + p: 0.03 + i: 0.002 + d: 0.0005 ``` The loop is advanced at a constant rate, specified by the `controllerAdjustmentTickRate` config option, which diff --git a/cmd/fan/init.go b/cmd/fan/init.go index 4647b6e..f1ce5fb 100644 --- a/cmd/fan/init.go +++ b/cmd/fan/init.go @@ -32,7 +32,8 @@ var initCmd = &cobra.Command{ p, fan, control_loop.NewDirectControlLoop(nil), - configuration.CurrentConfig.ControllerAdjustmentTickRate) + configuration.CurrentConfig.ControllerAdjustmentTickRate, + ) ui.Info("Deleting existing data for fan '%s'...", fan.GetId()) diff --git a/fan2go.yaml b/fan2go.yaml index eb26dcc..106c664 100644 --- a/fan2go.yaml +++ b/fan2go.yaml @@ -47,8 +47,8 @@ fans: # 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 # direct: the target value will be directly applied to the fan + # pid: uses a PID loop with default tuning variables controlAlgorithm: direct: # together with maxPwmChangePerCycle, fan speeds will approach target value @@ -84,6 +84,7 @@ fans: hwmon: platform: it8620 rpmChannel: 4 + controlAlgorithm: direct neverStop: true curve: case_avg_curve diff --git a/internal/backend.go b/internal/backend.go index ba813b3..33047a5 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -246,7 +246,7 @@ func initializeObjects(pers persistence.Persistence) map[fans.Fan]controller.Fan ) } else if config.ControlAlgorithm.Direct != nil { controlLoop = control_loop.NewDirectControlLoop( - &config.ControlAlgorithm.Direct.MaxPwmChangePerCycle, + config.ControlAlgorithm.Direct.MaxPwmChangePerCycle, ) } else { controlLoop = control_loop.NewDirectControlLoop(nil) diff --git a/internal/configuration/config.go b/internal/configuration/config.go index ecf955a..f2ecac0 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -123,7 +123,17 @@ func GetFilePath() string { func LoadConfig() { // load default configuration values CurrentConfig = Configuration{} - err := viper.Unmarshal(&CurrentConfig, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc())) + + err := viper.Unmarshal( + &CurrentConfig, + viper.DecodeHook( + mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + mapstructure.TextUnmarshallerHookFunc(), + ), + ), + ) if err != nil { ui.Fatal("unable to decode into struct, %v", err) } @@ -136,15 +146,21 @@ func (s *ControlAlgorithmConfig) UnmarshalText(text []byte) error { // check if the value matches one of the enum values switch controlAlgorithm { case string(Pid): - *s = ControlAlgorithmConfig{Pid: &PidControlAlgorithmConfig{}} + // default configuration for PID control algorithm + *s = ControlAlgorithmConfig{Pid: &PidControlAlgorithmConfig{ + 0.03, + 0.002, + 0.0005, + }} case string(Direct): + // default configuration for Direct control algorithm *s = ControlAlgorithmConfig{Direct: &DirectControlAlgorithmConfig{}} default: // if the value is not one of the enum values, try to unmarshal into a ControlAlgorithmConfig struct config := ControlAlgorithmConfig{} err := json.Unmarshal(text, &config) if err != nil { - return fmt.Errorf("invalid control algorithm: %s", controlAlgorithm) + return fmt.Errorf("invalid control algorithm config: %s", controlAlgorithm) } else { *s = config } diff --git a/internal/configuration/fans.go b/internal/configuration/fans.go index 54cd709..3399061 100644 --- a/internal/configuration/fans.go +++ b/internal/configuration/fans.go @@ -37,7 +37,7 @@ type ControlAlgorithmConfig struct { } type DirectControlAlgorithmConfig struct { - MaxPwmChangePerCycle int `json:"maxPwmChangePerCycle,omitempty"` + MaxPwmChangePerCycle *int `json:"maxPwmChangePerCycle,omitempty"` } type PidControlAlgorithmConfig struct { diff --git a/internal/configuration/validation.go b/internal/configuration/validation.go index d329e43..704108f 100644 --- a/internal/configuration/validation.go +++ b/internal/configuration/validation.go @@ -267,7 +267,8 @@ func validateFans(config *Configuration) error { if fanConfig.ControlAlgorithm != nil { if fanConfig.ControlAlgorithm.Direct != nil { - if fanConfig.ControlAlgorithm.Direct.MaxPwmChangePerCycle <= 0 { + maxPwmChangePerCycle := fanConfig.ControlAlgorithm.Direct.MaxPwmChangePerCycle + if maxPwmChangePerCycle != nil && *maxPwmChangePerCycle <= 0 { return fmt.Errorf("fan %s: invalid maxPwmChangePerCycle, must be > 0", fanConfig.ID) } }