diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 029f380..7576dca 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -22,13 +22,13 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '^1.18' + go-version: '^1.23' - name: Run golangci-lint uses: golangci/golangci-lint-action@v6.1.0 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.54.2 + version: v1.61.0 # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/Makefile b/Makefile index fb358c7..c07978a 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ PACKAGE := github.com/markusressel/$(NAME) GIT_REV ?= $(shell git rev-parse --short HEAD) SOURCE_DATE_EPOCH ?= $(shell date +%s) DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") -VERSION ?= 0.8.1 +VERSION ?= 0.9.0 test: ## Run all tests @go clean --testcache && go test -v ./... diff --git a/README.md b/README.md index 7851f59..71e4342 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ curves: Unlike the other curve types, this one does not use the average of the sensor data to calculate its value, which allows you to create a completely custom behaviour. -Keep in mind though that the fan controller is also PID based and will also affect +Keep in mind though that the fan controller may also be PID based which could also affect how the curve is applied to the fan. #### Function @@ -441,7 +441,7 @@ After successfully verifying your configuration you can launch fan2go from the C working as expected. Assuming you put your configuration file in `/etc/fan2go/fan2go.yaml` run: ```shell -> fan2go help 2 (0.032s) < 22:43:49 +> fan2go help fan2go is a simple daemon that controls the fans on your computer based on temperature sensors. @@ -654,12 +654,50 @@ 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 | -|--------|---------|----------| -| `0.03` | `0.002` | `0.0005` | +| P | I | D | +|-------|--------|---------| +| `0.3` | `0.02` | `0.005` | If you don't like the default behaviour you can configure your own in the config: @@ -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.3 + i: 0.02 + d: 0.005 ``` The loop is advanced at a constant rate, specified by the `controllerAdjustmentTickRate` config option, which diff --git a/cmd/curve/list.go b/cmd/curve/list.go index 2d3f19e..1f0a766 100644 --- a/cmd/curve/list.go +++ b/cmd/curve/list.go @@ -26,7 +26,7 @@ var curveCmd = &cobra.Command{ err = configuration.Validate(configPath) if err != nil { - ui.FatalWithoutStacktrace(err.Error()) + ui.FatalWithoutStacktrace("configuration validation failed: %v", err) } curveConfigsToPrint := []configuration.CurveConfig{} @@ -146,7 +146,7 @@ func drawGraph(graphValues map[int]float64, caption string) { } graph := asciigraph.Plot(values, asciigraph.Height(15), asciigraph.Width(100), asciigraph.Caption(caption)) - ui.Printfln(graph) + ui.Println(graph) } func printFunctionCurveInfo(curve curves.SpeedCurve, config *configuration.FunctionCurveConfig) { @@ -195,7 +195,7 @@ func printInfoTable(headers []string, rows [][]string) { panic(tableErr) } tableString := buf.String() - ui.Printfln(tableString) + ui.Println(tableString) } func init() { diff --git a/cmd/detect.go b/cmd/detect.go index eadaf9a..de31d81 100644 --- a/cmd/detect.go +++ b/cmd/detect.go @@ -122,9 +122,9 @@ var detectCmd = &cobra.Command{ } tableString := buf.String() if idx < (len(tables) - 1) { - ui.Printf(tableString) + ui.Print(tableString) } else { - ui.Printfln(tableString) + ui.Println(tableString) } } } diff --git a/cmd/fan/curve.go b/cmd/fan/curve.go index e137cff..6e23df5 100644 --- a/cmd/fan/curve.go +++ b/cmd/fan/curve.go @@ -24,7 +24,7 @@ var curveCmd = &cobra.Command{ configuration.LoadConfig() err := configuration.Validate(configPath) if err != nil { - ui.FatalWithoutStacktrace(err.Error()) + ui.FatalWithoutStacktrace("%v", err) } persistence := persistence.NewPersistence(configuration.CurrentConfig.DbPath) @@ -45,7 +45,7 @@ var curveCmd = &cobra.Command{ pwmData, fanCurveErr := persistence.LoadFanPwmData(fan) if fanCurveErr == nil { - _ = fan.AttachFanCurveData(&pwmData) + _ = fan.AttachFanRpmCurveData(&pwmData) } if idx > 0 { @@ -54,7 +54,7 @@ var curveCmd = &cobra.Command{ } // print table - ui.Printfln(fan.GetId()) + ui.Println(fan.GetId()) tab := table.Table{ Headers: []string{"", ""}, Rows: [][]string{ @@ -78,11 +78,11 @@ var curveCmd = &cobra.Command{ panic(tableErr) } tableString := buf.String() - ui.Printfln(tableString) + ui.Println(tableString) // print graph if fanCurveErr != nil { - ui.Printfln("No fan curve data yet...") + ui.Println("No fan curve data yet...") continue } @@ -99,7 +99,7 @@ var curveCmd = &cobra.Command{ caption := "RPM / PWM" graph := asciigraph.Plot(values, asciigraph.Height(15), asciigraph.Width(100), asciigraph.Caption(caption)) - ui.Printfln(graph) + ui.Println(graph) } }, } diff --git a/cmd/fan/fan.go b/cmd/fan/fan.go index 81c85b8..c75898a 100644 --- a/cmd/fan/fan.go +++ b/cmd/fan/fan.go @@ -35,7 +35,7 @@ func getFan(id string) (fans.Fan, error) { configuration.LoadConfig() err := configuration.Validate(configPath) if err != nil { - ui.FatalWithoutStacktrace(err.Error()) + ui.FatalWithoutStacktrace("%v", err) } controllers := hwmon.GetChips() diff --git a/cmd/fan/init.go b/cmd/fan/init.go index 8062e07..f1ce5fb 100644 --- a/cmd/fan/init.go +++ b/cmd/fan/init.go @@ -2,10 +2,10 @@ package fan import ( "github.com/markusressel/fan2go/internal/configuration" + "github.com/markusressel/fan2go/internal/control_loop" "github.com/markusressel/fan2go/internal/controller" "github.com/markusressel/fan2go/internal/persistence" "github.com/markusressel/fan2go/internal/ui" - "github.com/markusressel/fan2go/internal/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -31,12 +31,9 @@ var initCmd = &cobra.Command{ fanController := controller.NewFanController( p, fan, - *util.NewPidLoop( - 0.03, - 0.002, - 0.0005, - ), - configuration.CurrentConfig.ControllerAdjustmentTickRate) + control_loop.NewDirectControlLoop(nil), + configuration.CurrentConfig.ControllerAdjustmentTickRate, + ) ui.Info("Deleting existing data for fan '%s'...", fan.GetId()) diff --git a/cmd/root.go b/cmd/root.go index ad6dea1..b2fe74c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,7 +33,7 @@ on your computer based on temperature sensors.`, configuration.LoadConfig() err := configuration.Validate(configPath) if err != nil { - ui.ErrorAndNotify("Config Validation Error", err.Error()) + ui.ErrorAndNotify("Config Validation Error: %v", "%v", err) return } @@ -75,6 +75,7 @@ func printHeader() { if err != nil { fmt.Println("fan2go") } + ui.Info("Version: %s", global.Version) } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/sensor/sensor.go b/cmd/sensor/sensor.go index 46ad2dc..3a318b0 100644 --- a/cmd/sensor/sensor.go +++ b/cmd/sensor/sensor.go @@ -52,7 +52,7 @@ func getSensor(id string) (sensors.Sensor, error) { configuration.LoadConfig() err := configuration.Validate(configPath) if err != nil { - ui.FatalWithoutStacktrace(err.Error()) + ui.FatalWithoutStacktrace("%v", err) } controllers := hwmon.GetChips() diff --git a/fan2go.yaml b/fan2go.yaml index 3e3479b..106c664 100644 --- a/fan2go.yaml +++ b/fan2go.yaml @@ -46,6 +46,14 @@ 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. + # 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 + # with the given max speed. + maxPwmChangePerCycle: 10 # (Optional) Override for the lowest PWM value at which the # fan is able to maintain rotation if it was spinning previously. minPwm: 30 @@ -76,6 +84,7 @@ fans: hwmon: platform: it8620 rpmChannel: 4 + controlAlgorithm: direct neverStop: true curve: case_avg_curve diff --git a/go.mod b/go.mod index bb2816a..74ba810 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/markusressel/fan2go -go 1.22 - -toolchain go1.22.5 +go 1.23.1 require ( github.com/asecurityteam/rolling v2.0.4+incompatible @@ -13,15 +11,18 @@ require ( github.com/md14454/gosensors v0.0.0-20180726083412-bded752ab001 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.5.0 github.com/oklog/run v1.1.0 + github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/prometheus/client_golang v1.20.4 github.com/pterm/pterm v0.12.79 + github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/tomlazar/table v0.1.2 go.etcd.io/bbolt v1.3.11 - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) require ( @@ -30,45 +31,44 @@ require ( atomicgo.dev/schedule v0.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/console v1.0.3 // indirect + github.com/containerd/console v1.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.10 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.59.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.6.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9621983..4caa486 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,9 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -47,8 +48,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -83,8 +84,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/md14454/gosensors v0.0.0-20180726083412-bded752ab001 h1:QLG/T9lLD94S4N/AfPxIqsEiNO4V2rlHJ79K/LFuh+s= github.com/md14454/gosensors v0.0.0-20180726083412-bded752ab001/go.mod h1:VVwwo3ihK1PrIz7y142tFX/PNtaKb41FYyS78lQEiLQ= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= @@ -97,8 +98,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -106,8 +109,8 @@ github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zI github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= @@ -119,14 +122,16 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc= +github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= @@ -135,8 +140,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -144,15 +149,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -173,23 +172,23 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -203,26 +202,27 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/api/curves.go b/internal/api/curves.go index 535d0d2..e0864a9 100644 --- a/internal/api/curves.go +++ b/internal/api/curves.go @@ -17,13 +17,13 @@ func registerCurveEndpoints(rest *echo.Echo) { } func getCurves(c echo.Context) error { - data := curves.SpeedCurveMap + data := curves.SnapshotSpeedCurveMap() return c.JSONPretty(http.StatusOK, data, indentationChar) } func getCurve(c echo.Context) error { id := c.Param(urlParamId) - data, exists := curves.SpeedCurveMap[id] + data, exists := curves.GetSpeedCurve(id) if !exists { return returnNotFound(c, id) } else { diff --git a/internal/api/fans.go b/internal/api/fans.go index 841b315..b00a89d 100644 --- a/internal/api/fans.go +++ b/internal/api/fans.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/labstack/echo/v4" "github.com/markusressel/fan2go/internal/fans" + "github.com/qdm12/reprint" "net/http" ) @@ -18,13 +19,13 @@ func registerFanEndpoints(rest *echo.Echo) { // returns a list of all currently configured fans func getFans(c echo.Context) error { - data := fans.FanMap + data := reprint.This(fans.SnapshotFanMap()) return c.JSONPretty(http.StatusOK, data, indentationChar) } func getFan(c echo.Context) error { id := c.Param(urlParamId) - data, exists := fans.FanMap[id] + data, exists := fans.GetFan(id) if !exists { return returnNotFound(c, id) } else { diff --git a/internal/api/sensors.go b/internal/api/sensors.go index 88b5e0d..0db661d 100644 --- a/internal/api/sensors.go +++ b/internal/api/sensors.go @@ -17,14 +17,14 @@ func registerSensorEndpoints(rest *echo.Echo) { } func getSensors(c echo.Context) error { - data := sensors.SensorMap + data := sensors.SnapshotSensorMap() return c.JSONPretty(http.StatusOK, data, indentationChar) } func getSensor(c echo.Context) error { id := c.Param(urlParamId) - data, exists := sensors.SensorMap[id] + data, exists := sensors.GetSensor(id) if !exists { return returnNotFound(c, id) } else { diff --git a/internal/backend.go b/internal/backend.go index 9a8ee4f..76568d9 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -3,6 +3,7 @@ package internal import ( "context" "fmt" + "github.com/markusressel/fan2go/internal/control_loop" "net/http" "net/http/pprof" "os" @@ -23,7 +24,6 @@ import ( "github.com/markusressel/fan2go/internal/sensors" "github.com/markusressel/fan2go/internal/statistics" "github.com/markusressel/fan2go/internal/ui" - "github.com/markusressel/fan2go/internal/util" "github.com/oklog/run" ) @@ -37,7 +37,10 @@ func RunDaemon() { pers := persistence.NewPersistence(configuration.CurrentConfig.DbPath) - fanControllers := initializeObjects(pers) + fanControllers, err := initializeObjects(pers) + if err != nil { + ui.Fatal("Error initializing objects: %v", err) + } ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -65,7 +68,7 @@ func RunDaemon() { return nil }, func(err error) { if err != nil { - ui.Warning("Error stopping parca webserver: " + err.Error()) + ui.Warning("Error stopping parca webserver: %v", err) } else { ui.Debug("Webservers stopped.") } @@ -94,7 +97,7 @@ func RunDaemon() { return nil }, func(err error) { if err != nil { - ui.Warning("Error stopping webservers: " + err.Error()) + ui.Warning("Error stopping webservers: %v", err) } else { ui.Debug("Webservers stopped.") } @@ -103,7 +106,8 @@ func RunDaemon() { } { // === sensor monitoring - for _, sensor := range sensors.SensorMap { + sensorMapData := sensors.SnapshotSensorMap() + for _, sensor := range sensorMapData { s := sensor pollingRate := configuration.CurrentConfig.TempSensorPollingRate mon := NewSensorMonitor(s, pollingRate) @@ -142,7 +146,7 @@ func RunDaemon() { }) } - if len(fans.FanMap) == 0 { + if len(fans.SnapshotFanMap()) == 0 { ui.FatalWithoutStacktrace("No valid fan configurations, exiting.") } } @@ -217,32 +221,59 @@ func startStatisticsServer() *echo.Echo { return echoPrometheus } -func initializeObjects(pers persistence.Persistence) map[fans.Fan]controller.FanController { +func initializeObjects(pers persistence.Persistence) (map[fans.Fan]controller.FanController, error) { controllers := hwmon.GetChips() - initializeSensors(controllers) - initializeCurves() + err := initializeSensors(controllers) + if err != nil { + return nil, fmt.Errorf("error initializing sensors: %v", err) + } + err = initializeCurves() + if err != nil { + return nil, fmt.Errorf("error initializing curves: %v", err) + } var result = map[fans.Fan]controller.FanController{} + fanMap, err := initializeFans(controllers) + if err != nil { + return nil, fmt.Errorf("error initializing fans: %v", err) + } - for config, fan := range initializeFans(controllers) { + for config, fan := range fanMap { updateRate := configuration.CurrentConfig.ControllerAdjustmentTickRate - var pidLoop util.PidLoop - if config.ControlLoop != nil { - pidLoop = *util.NewPidLoop( - config.ControlLoop.P, - config.ControlLoop.I, - config.ControlLoop.D, + var controlLoop control_loop.ControlLoop + + // compatibility fallback + if config.ControlLoop != nil { //nolint:all + ui.Warning("Using deprecated control loop configuration for fan %s. Please update your configuration to use the new control algorithm configuration.", config.ID) + controlLoop = control_loop.NewPidControlLoop( + + config.ControlLoop.P, //nolint:all + config.ControlLoop.I, //nolint:all + config.ControlLoop.D, //nolint:all ) + } else if config.ControlAlgorithm != nil { + if config.ControlAlgorithm.Pid != nil { + controlLoop = control_loop.NewPidControlLoop( + config.ControlAlgorithm.Pid.P, + config.ControlAlgorithm.Pid.I, + config.ControlAlgorithm.Pid.D, + ) + } else if config.ControlAlgorithm.Direct != nil { + controlLoop = control_loop.NewDirectControlLoop( + config.ControlAlgorithm.Direct.MaxPwmChangePerCycle, + ) + } } else { - pidLoop = *util.NewPidLoop( - 0.03, - 0.002, - 0.0005, + controlLoop = control_loop.NewPidControlLoop( + control_loop.DefaultPidConfig.P, + control_loop.DefaultPidConfig.I, + control_loop.DefaultPidConfig.D, ) } - fanController := controller.NewFanController(pers, fan, pidLoop, updateRate) + + fanController := controller.NewFanController(pers, fan, controlLoop, updateRate) result[fan] = fanController } @@ -253,10 +284,10 @@ func initializeObjects(pers persistence.Persistence) map[fans.Fan]controller.Fan controllerCollector := statistics.NewControllerCollector(fanControllers) statistics.Register(controllerCollector) - return result + return result, nil } -func initializeSensors(controllers []*hwmon.HwMonController) { +func initializeSensors(controllers []*hwmon.HwMonController) error { var sensorList []sensors.Sensor for _, config := range configuration.CurrentConfig.Sensors { if config.HwMon != nil { @@ -264,7 +295,7 @@ func initializeSensors(controllers []*hwmon.HwMonController) { for _, c := range controllers { matched, err := regexp.MatchString("(?i)"+config.HwMon.Platform, c.Platform) if err != nil { - ui.Fatal("Failed to match platform regex of %s (%s) against controller platform %s", config.ID, config.HwMon.Platform, c.Platform) + return fmt.Errorf("failed to match platform regex of %s (%s) against controller platform %s: %v", config.ID, config.HwMon.Platform, c.Platform, err) } if matched { found = true @@ -272,13 +303,13 @@ func initializeSensors(controllers []*hwmon.HwMonController) { } } if !found { - ui.Fatal("Couldn't find hwmon device with platform '%s' for sensor: %s. Run 'fan2go detect' again and correct any mistake.", config.HwMon.Platform, config.ID) + return fmt.Errorf("couldn't find hwmon device with platform '%s' for sensor: %s. Run 'fan2go detect' again and correct any mistake", config.HwMon.Platform, config.ID) } } sensor, err := sensors.NewSensor(config) if err != nil { - ui.Fatal("Unable to process sensor configuration: %s", config.ID) + return fmt.Errorf("unable to process sensor configuration: %s", config.ID) } sensorList = append(sensorList, sensor) @@ -288,29 +319,33 @@ func initializeSensors(controllers []*hwmon.HwMonController) { } sensor.SetMovingAvg(currentValue) - sensors.SensorMap[config.ID] = sensor + sensors.RegisterSensor(sensor) } sensorCollector := statistics.NewSensorCollector(sensorList) statistics.Register(sensorCollector) + + return nil } -func initializeCurves() { +func initializeCurves() error { var curveList []curves.SpeedCurve for _, config := range configuration.CurrentConfig.Curves { curve, err := curves.NewSpeedCurve(config) if err != nil { - ui.Fatal("Unable to process curve configuration: %s", config.ID) + return fmt.Errorf("unable to process curve configuration: %s: %v", config.ID, err) } curveList = append(curveList, curve) - curves.SpeedCurveMap[config.ID] = curve + curves.RegisterSpeedCurve(curve) } curveCollector := statistics.NewCurveCollector(curveList) statistics.Register(curveCollector) + + return nil } -func initializeFans(controllers []*hwmon.HwMonController) map[configuration.FanConfig]fans.Fan { +func initializeFans(controllers []*hwmon.HwMonController) (map[configuration.FanConfig]fans.Fan, error) { var result = map[configuration.FanConfig]fans.Fan{} var fanList []fans.Fan @@ -319,15 +354,15 @@ func initializeFans(controllers []*hwmon.HwMonController) map[configuration.FanC if config.HwMon != nil { err := hwmon.UpdateFanConfigFromHwMonControllers(controllers, &config) if err != nil { - ui.Fatal("Couldn't update fan config from hwmon: %s", err) + return nil, fmt.Errorf("couldn't update fan config from hwmon: %v", err) } } fan, err := fans.NewFan(config) if err != nil { - ui.Fatal("Unable to process fan configuration of '%s': %v", config.ID, err) + return nil, fmt.Errorf("unable to process fan configuration of '%s': %v", config.ID, err) } - fans.FanMap[config.ID] = fan + fans.RegisterFan(fan) result[config] = fan fanList = append(fanList, fan) @@ -336,7 +371,7 @@ func initializeFans(controllers []*hwmon.HwMonController) map[configuration.FanC fanCollector := statistics.NewFanCollector(fanList) statistics.Register(fanCollector) - return result + return result, nil } func getProcessOwner() (string, error) { diff --git a/internal/configuration/config.go b/internal/configuration/config.go index 791ab27..71f8978 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -1,6 +1,10 @@ package configuration import ( + "encoding/json" + "fmt" + "github.com/markusressel/fan2go/internal/control_loop" + "github.com/mitchellh/mapstructure" "os" "time" @@ -120,8 +124,54 @@ func GetFilePath() string { func LoadConfig() { // load default configuration values CurrentConfig = Configuration{} - err := viper.Unmarshal(&CurrentConfig) + + 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) } } + +// UnmarshalText is a custom unmarshaler for ControlAlgorithmConfig to handle string enum values +func (s *ControlAlgorithmConfig) UnmarshalText(text []byte) error { + controlAlgorithm := string(text) + + // check if the value matches one of the enum values + switch controlAlgorithm { + case string(Pid): + // default configuration for PID control algorithm + *s = ControlAlgorithmConfig{ + Pid: &PidControlAlgorithmConfig{ + control_loop.DefaultPidConfig.P, + control_loop.DefaultPidConfig.I, + control_loop.DefaultPidConfig.D, + }, + } + case string(Direct): + // default configuration for Direct control algorithm + *s = ControlAlgorithmConfig{ + Direct: &DirectControlAlgorithmConfig{ + nil, + }, + } + 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 config: %s", controlAlgorithm) + } else { + *s = config + } + } + + return nil +} diff --git a/internal/configuration/fans.go b/internal/configuration/fans.go index 16ec978..3399061 100644 --- a/internal/configuration/fans.go +++ b/internal/configuration/fans.go @@ -8,15 +8,44 @@ 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"` + 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 is a configuration for a PID control loop. + // + // Deprecated: HeaderMap exists for historical compatibility + // and should not be used. To access the headers returned by a handler, + // use the Response.Header map as returned by the Result method. ControlLoop *ControlLoopConfig `json:"controlLoop,omitempty"` } +type ControlAlgorithm string + +const ( + Pid ControlAlgorithm = "pid" + Direct ControlAlgorithm = "direct" +) + +type ControlAlgorithmConfig struct { + Direct *DirectControlAlgorithmConfig `json:"direct,omitempty"` + Pid *PidControlAlgorithmConfig `json:"pid,omitempty"` +} + +type DirectControlAlgorithmConfig struct { + MaxPwmChangePerCycle *int `json:"maxPwmChangePerCycle,omitempty"` +} + +type PidControlAlgorithmConfig struct { + P float64 `json:"p"` + I float64 `json:"i"` + D float64 `json:"d"` +} + type HwMonFanConfig struct { Platform string `json:"platform"` Index int `json:"index"` diff --git a/internal/configuration/validation.go b/internal/configuration/validation.go index cec0f6d..704108f 100644 --- a/internal/configuration/validation.go +++ b/internal/configuration/validation.go @@ -265,6 +265,22 @@ func validateFans(config *Configuration) error { return fmt.Errorf("fan %s: no curve definition with id '%s' found", fanConfig.ID, fanConfig.Curve) } + if fanConfig.ControlAlgorithm != nil { + if fanConfig.ControlAlgorithm.Direct != nil { + maxPwmChangePerCycle := fanConfig.ControlAlgorithm.Direct.MaxPwmChangePerCycle + if maxPwmChangePerCycle != nil && *maxPwmChangePerCycle <= 0 { + return fmt.Errorf("fan %s: invalid maxPwmChangePerCycle, must be > 0", fanConfig.ID) + } + } + + if fanConfig.ControlAlgorithm.Pid != nil { + pidConfig := fanConfig.ControlAlgorithm.Pid + if pidConfig.P == 0 && pidConfig.I == 0 && pidConfig.D == 0 { + return fmt.Errorf("fan %s: all PID constants are zero", fanConfig.ID) + } + } + } + if fanConfig.HwMon != nil { if (fanConfig.HwMon.Index != 0 && fanConfig.HwMon.RpmChannel != 0) || (fanConfig.HwMon.Index == 0 && fanConfig.HwMon.RpmChannel == 0) { return fmt.Errorf("fan %s: must have one of index or rpmChannel, must be >= 1", fanConfig.ID) diff --git a/internal/control_loop/common.go b/internal/control_loop/common.go new file mode 100644 index 0000000..56fcf76 --- /dev/null +++ b/internal/control_loop/common.go @@ -0,0 +1,9 @@ +package control_loop + +// ControlLoop defines how a FanController approaches the target value of its curve. +type ControlLoop interface { + // Cycle advances the control loop to the next step and returns the new pwm value. + // Note: multiple calls to Loop may not result in the same output, as + // the control loop may take time into account or have other stateful properties. + Cycle(target int, current int) int +} diff --git a/internal/control_loop/direct.go b/internal/control_loop/direct.go new file mode 100644 index 0000000..4cc093f --- /dev/null +++ b/internal/control_loop/direct.go @@ -0,0 +1,62 @@ +package control_loop + +import ( + "github.com/markusressel/fan2go/internal/util" + "math" + "time" +) + +// DirectControlLoop is a very simple control that directly applies the given +// target pwm. It can also be used to gracefully approach the target by +// utilizing the "maxPwmChangePerCycle" property. +type DirectControlLoop struct { + // limits the maximum allowed pwm change per cycle + maxPwmChangePerCycle *int + lastTime time.Time +} + +// NewDirectControlLoop creates a DirectControlLoop, which is a very simple control that directly applies the given +// target pwm. It can also be used to gracefully approach the target by +// utilizing the "maxPwmChangePerCycle" property. +func NewDirectControlLoop( + // (optional) limits the maximum allowed pwm change per cycle (in both directions) + maxPwmChangePerCycle *int, +) *DirectControlLoop { + return &DirectControlLoop{ + maxPwmChangePerCycle: maxPwmChangePerCycle, + lastTime: time.Now(), + } +} + +func (l *DirectControlLoop) Cycle(target int, current int) int { + loopTime := time.Now() + + dt := loopTime.Sub(l.lastTime).Seconds() + + l.lastTime = loopTime + + var stepTarget = float64(target) + if l.maxPwmChangePerCycle != nil { + // the pwm adjustment depends on the direction and + // the time-based change speed limit. + stepTarget = float64(*l.maxPwmChangePerCycle) * dt + + err := float64(target - current) + // 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 + stepTarget = util.Coerce(stepTarget, 0, err) + } else { + // above or at desired speed, subtract pwms + stepTarget = util.Coerce(-stepTarget, err, 0) + } + } + + // ensure we are within sane bounds + coerced := util.Coerce(stepTarget, 0, 255) + result := int(math.Round(coerced)) + + return result +} diff --git a/internal/control_loop/pid.go b/internal/control_loop/pid.go new file mode 100644 index 0000000..437d873 --- /dev/null +++ b/internal/control_loop/pid.go @@ -0,0 +1,46 @@ +package control_loop + +import ( + "github.com/markusressel/fan2go/internal/util" + "math" +) + +type PidControlLoopDefaults struct { + P float64 + I float64 + D float64 +} + +var ( + DefaultPidConfig = PidControlLoopDefaults{ + P: 0.3, + I: 0.02, + D: 0.005, + } +) + +// PidControlLoop is a PidLoop based control loop implementation. +type PidControlLoop struct { + pidLoop *util.PidLoop +} + +// NewPidControlLoop creates a PidControlLoop, which uses a PID loop to approach the target. +func NewPidControlLoop( + p float64, + i float64, + d float64, +) *PidControlLoop { + return &PidControlLoop{ + pidLoop: util.NewPidLoop(p, i, d), + } +} + +func (l *PidControlLoop) Cycle(target int, current int) int { + result := l.pidLoop.Loop(float64(target), float64(current)) + + // ensure we are within sane bounds + coerced := util.Coerce(float64(current)+result, 0, 255) + stepTarget := int(math.Round(coerced)) + + return stepTarget +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 0bee1db..de0322a 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -2,7 +2,9 @@ package controller import ( "context" + "errors" "fmt" + "github.com/markusressel/fan2go/internal/control_loop" "math" "sort" "sync" @@ -20,6 +22,10 @@ import ( // Amount of time to wait between a set-pwm and get-pwm. Used during fan initial calibration. const pwmSetGetDelay time.Duration = 5 * time.Millisecond +var ( + ErrFanStalledAtMaxPwm = errors.New("fan stalled at max pwm") +) + var InitializationSequenceMutex sync.Mutex type FanControllerStatistics struct { @@ -42,7 +48,7 @@ type FanController interface { UpdateFanSpeed() error } -type PidFanController struct { +type DefaultFanController struct { // controller statistics stats FanControllerStatistics // persistence where fan data is stored @@ -64,8 +70,9 @@ type PidFanController struct { pwmValuesWithDistinctTarget []int // a map of x -> getPwm() where x is setPwm(x) for the controlled fan pwmMap map[int]int - // PID loop for the PWM control - pidLoop *util.PidLoop + + // control loop that specifies how the target value of the curve is approached + controlLoop control_loop.ControlLoop // offset applied to the actual minPwm of the fan to ensure "neverStops" constraint minPwmOffset int @@ -74,30 +81,34 @@ type PidFanController struct { func NewFanController( persistence persistence.Persistence, fan fans.Fan, - pidLoop util.PidLoop, + controlLoop control_loop.ControlLoop, updateRate time.Duration, ) FanController { - return &PidFanController{ + curve, ok := curves.GetSpeedCurve(fan.GetCurveId()) + if !ok { + ui.Fatal("Failed to create fan controller for fan '%s': Curve with ID '%s' not found", fan.GetId(), fan.GetCurveId()) + } + return &DefaultFanController{ persistence: persistence, fan: fan, - curve: curves.SpeedCurveMap[fan.GetCurveId()], + curve: curve, updateRate: updateRate, pwmValuesWithDistinctTarget: []int{}, pwmMap: nil, - pidLoop: &pidLoop, + controlLoop: controlLoop, minPwmOffset: 0, } } -func (f *PidFanController) GetFanId() string { +func (f *DefaultFanController) GetFanId() string { return f.fan.GetId() } -func (f *PidFanController) GetStatistics() FanControllerStatistics { +func (f *DefaultFanController) GetStatistics() FanControllerStatistics { return f.stats } -func (f *PidFanController) Run(ctx context.Context) error { +func (f *DefaultFanController) Run(ctx context.Context) error { err := f.persistence.Init() if err != nil { return err @@ -154,7 +165,7 @@ func (f *PidFanController) Run(ctx context.Context) error { return err } - err = fan.AttachFanCurveData(&fanPwmData) + err = fan.AttachFanRpmCurveData(&fanPwmData) if err != nil { return err } @@ -228,46 +239,27 @@ func (f *PidFanController) Run(ctx context.Context) error { return err } -func (f *PidFanController) UpdateFanSpeed() error { +func (f *DefaultFanController) UpdateFanSpeed() error { fan := f.fan - lastSetPwm := 0 - if f.lastSetPwm != nil { - lastSetPwm = *(f.lastSetPwm) - } else { - pwm, err := f.fan.GetPwm() - if err != nil { - return err - } - lastSetPwm = pwm - } - // 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 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) - roundedTarget := int(math.Round(coerced)) + target, err := f.calculateTargetPwm() + if err != nil { + return err + } - if target >= 0 { - _ = trySetManualPwm(f.fan) - err := f.setPwm(roundedTarget) - if err != nil { - ui.Error("Error setting %s: %v", fan.GetId(), err) - } + _ = trySetManualPwm(f.fan) + err = f.setPwm(target) + if err != nil { + // TODO: maybe we should add some kind of critical failure mode here + // in case these errors don't resolve after a while + ui.Error("Error setting %s: %v", fan.GetId(), err) } return nil } -func (f *PidFanController) RunInitializationSequence() (err error) { +func (f *DefaultFanController) RunInitializationSequence() (err error) { fan := f.fan err1 := f.computePwmMap() @@ -336,7 +328,7 @@ func (f *PidFanController) RunInitializationSequence() (err error) { ui.Debug("Measured RPM of %d at PWM %d for fan %s", int(fan.GetRpmAvg()), pwm, fan.GetId()) } - err = fan.AttachFanCurveData(&curveData) + err = fan.AttachFanRpmCurveData(&curveData) if err != nil { ui.Error("Failed to attach fan curve data to fan %s: %v", fan.GetId(), err) return err @@ -364,8 +356,7 @@ func measureRpm(fan fans.Fan) { updatedRpmAvg := util.UpdateSimpleMovingAvg(fan.GetRpmAvg(), configuration.CurrentConfig.RpmRollingWindowSize, float64(rpm)) fan.SetRpmAvg(updatedRpmAvg) - pwmRpmMap := fan.GetFanCurveData() - (*pwmRpmMap)[pwm] = float64(rpm) + fan.UpdateFanRpmCurveValue(pwm, float64(rpm)) } func trySetManualPwm(fan fans.Fan) error { @@ -384,7 +375,7 @@ func trySetManualPwm(fan fans.Fan) error { return err } -func (f *PidFanController) restorePwmEnabled() { +func (f *DefaultFanController) restorePwmEnabled() { ui.Info("Trying to restore fan settings for %s...", f.fan.GetId()) err := f.fan.SetPwm(f.originalPwmValue) @@ -407,14 +398,28 @@ func (f *PidFanController) restorePwmEnabled() { } // calculates the optimal pwm for a fan with the given target level. -// returns -1 if no rpm is detected even at fan.maxPwm -func (f *PidFanController) calculateTargetPwm() int { +// returns ErrFanStalledAtMaxPwm if no rpm is detected even at fan.maxPwm +func (f *DefaultFanController) calculateTargetPwm() (int, error) { + lastSetPwm := 0 + if f.lastSetPwm != nil { + lastSetPwm = *(f.lastSetPwm) + } else { + pwm, err := f.fan.GetPwm() + if err != nil { + return -1, err + } + lastSetPwm = pwm + } + fan := f.fan target, err := f.curve.Evaluate() if err != nil { ui.Fatal("Unable to calculate optimal PWM value for %s: %v", fan.GetId(), err) } + // the target pwm, approaching the actual target smoothly + target = f.controlLoop.Cycle(target, lastSetPwm) + // ensure target value is within bounds of possible values if target > fans.MaxPwmValue { ui.Warning("Tried to set out-of-bounds PWM value %d on fan %s", target, fan.GetId()) @@ -428,7 +433,9 @@ func (f *PidFanController) calculateTargetPwm() int { maxPwm := fan.GetMaxPwm() minPwm := fan.GetMinPwm() + f.minPwmOffset + // determine the target value based on the pwm range as well as RPM curve of the fan // TODO: this assumes a linear curve, but it might be something else + // TODO: remove target = minPwm + int((float64(target)/fans.MaxPwmValue)*(float64(maxPwm)-float64(minPwm))) if f.lastSetPwm != nil && f.pwmMap != nil { @@ -452,7 +459,7 @@ func (f *PidFanController) calculateTargetPwm() int { if avgRpm <= 0 { if target >= maxPwm { ui.Error("CRITICAL: Fan %s avg. RPM is %d, even at PWM value %d", fan.GetId(), int(avgRpm), target) - return -1 + return -1, ErrFanStalledAtMaxPwm } oldOffset := f.minPwmOffset ui.Warning("WARNING: Increasing minPWM of %s from %d to %d, which is supposed to never stop, but RPM is %d", @@ -468,11 +475,11 @@ func (f *PidFanController) calculateTargetPwm() int { } } - return target + return target, nil } // set the pwm speed of a fan to the specified value (0..255) -func (f *PidFanController) setPwm(target int) (err error) { +func (f *DefaultFanController) setPwm(target int) (err error) { current, err := f.fan.GetPwm() closestTarget := f.findClosestDistinctTarget(target) @@ -487,7 +494,7 @@ func (f *PidFanController) setPwm(target int) (err error) { } } -func (f *PidFanController) waitForFanToSettle(fan fans.Fan) { +func (f *DefaultFanController) waitForFanToSettle(fan fans.Fan) { // TODO: this "waiting" logic could also be applied to the other measurements diffThreshold := configuration.CurrentConfig.MaxRpmDiffForSettledFan @@ -517,12 +524,12 @@ func (f *PidFanController) waitForFanToSettle(fan fans.Fan) { // // Note: The value returned by this method must be used as the key // to the pwmMap to get the actual target pwm value for the fan of this controller. -func (f *PidFanController) findClosestDistinctTarget(target int) int { +func (f *DefaultFanController) findClosestDistinctTarget(target int) int { return util.FindClosest(target, f.pwmValuesWithDistinctTarget) } // computePwmMap computes a mapping between "requested pwm value" -> "actual set pwm value" -func (f *PidFanController) computePwmMap() (err error) { +func (f *DefaultFanController) computePwmMap() (err error) { if !configuration.CurrentConfig.RunFanInitializationInParallel { InitializationSequenceMutex.Lock() defer InitializationSequenceMutex.Unlock() @@ -573,7 +580,7 @@ func (f *PidFanController) computePwmMap() (err error) { return f.persistence.SaveFanPwmMap(f.fan.GetId(), f.pwmMap) } -func (f *PidFanController) computePwmMapAutomatically() { +func (f *DefaultFanController) computePwmMapAutomatically() { fan := f.fan _ = trySetManualPwm(fan) @@ -593,7 +600,7 @@ func (f *PidFanController) computePwmMapAutomatically() { _ = fan.SetPwm(f.applyPwmMapping(fan.GetStartPwm())) } -func (f *PidFanController) updateDistinctPwmValues() { +func (f *DefaultFanController) updateDistinctPwmValues() { var keys = util.ExtractKeysWithDistinctValues(f.pwmMap) sort.Ints(keys) f.pwmValuesWithDistinctTarget = keys @@ -601,12 +608,12 @@ func (f *PidFanController) updateDistinctPwmValues() { ui.Debug("Distinct PWM value targets of fan %s: %v", f.fan.GetId(), keys) } -func (f *PidFanController) increaseMinPwmOffset() { +func (f *DefaultFanController) increaseMinPwmOffset() { f.minPwmOffset += 1 f.stats.MinPwmOffset = f.minPwmOffset f.stats.IncreasedMinPwmCount += 1 } -func (f *PidFanController) applyPwmMapping(target int) int { +func (f *DefaultFanController) applyPwmMapping(target int) int { return f.pwmMap[target] } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 149552c..74d03e9 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -2,6 +2,7 @@ package controller import ( "errors" + "github.com/markusressel/fan2go/internal/control_loop" "sort" "testing" "time" @@ -114,15 +115,22 @@ func (fan *MockFan) SetPwm(pwm int) (err error) { return nil } -func (fan MockFan) GetFanCurveData() *map[int]float64 { +func (fan MockFan) GetFanRpmCurveData() *map[int]float64 { return fan.speedCurve } -func (fan *MockFan) AttachFanCurveData(curveData *map[int]float64) (err error) { +func (fan *MockFan) AttachFanRpmCurveData(curveData *map[int]float64) (err error) { fan.speedCurve = curveData return err } +func (fan *MockFan) UpdateFanRpmCurveValue(pwm int, rpm float64) { + if (fan.speedCurve) == nil { + fan.speedCurve = &map[int]float64{} + } + (*fan.speedCurve)[pwm] = rpm +} + func (fan MockFan) GetPwmEnabled() (int, error) { return int(fan.ControlMode), nil } @@ -361,9 +369,9 @@ func CreateFan(neverStop bool, curveData map[int]float64, startPwm *int) (fan fa }, StartPwm: startPwm, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) - err = fan.AttachFanCurveData(&curveData) + err = fan.AttachFanRpmCurveData(&curveData) return fan, err } @@ -424,14 +432,14 @@ func TestCalculateTargetSpeedLinear(t *testing.T) { Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(&s) curveValue := 127 - curve := MockCurve{ + curve := &MockCurve{ ID: "curve", Value: curveValue, } - curves.SpeedCurveMap[curve.GetId()] = &curve + curves.RegisterSpeedCurve(curve) fan := &MockFan{ ID: "fan", @@ -440,21 +448,25 @@ func TestCalculateTargetSpeedLinear(t *testing.T) { curveId: curve.GetId(), speedCurve: &LinearFan, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) + + controlLoop := control_loop.NewDirectControlLoop(nil) - controller := PidFanController{ + controller := DefaultFanController{ persistence: mockPersistence{}, fan: fan, curve: curve, updateRate: time.Duration(100), + controlLoop: controlLoop, pwmMap: createOneToOnePwmMap(), } controller.updateDistinctPwmValues() // WHEN - optimal := controller.calculateTargetPwm() + optimal, err := controller.calculateTargetPwm() // THEN + assert.NoError(t, err) assert.Equal(t, 127, optimal) } @@ -462,19 +474,19 @@ func TestCalculateTargetSpeedNeverStop(t *testing.T) { // GIVEN avgTmp := 40000.0 - s := MockSensor{ + s := &MockSensor{ ID: "sensor", Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveValue := 0 curve := &MockCurve{ ID: "curve", Value: curveValue, } - curves.SpeedCurveMap[curve.GetId()] = curve + curves.RegisterSpeedCurve(curve) fan := &MockFan{ ID: "fan", @@ -485,21 +497,25 @@ func TestCalculateTargetSpeedNeverStop(t *testing.T) { shouldNeverStop: true, speedCurve: &NeverStoppingFan, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) + + controlLoop := control_loop.NewDirectControlLoop(nil) - controller := PidFanController{ + controller := DefaultFanController{ persistence: mockPersistence{}, fan: fan, curve: curve, updateRate: time.Duration(100), + controlLoop: controlLoop, pwmMap: createOneToOnePwmMap(), } controller.updateDistinctPwmValues() // WHEN - target := controller.calculateTargetPwm() + target, err := controller.calculateTargetPwm() // THEN + assert.NoError(t, err) assert.Greater(t, fan.GetMinPwm(), 0) assert.Equal(t, fan.GetMinPwm(), target) } @@ -533,19 +549,19 @@ func TestFanController_UpdateFanSpeed_FanCurveGaps(t *testing.T) { // GIVEN avgTmp := 40000.0 - s := MockSensor{ + s := &MockSensor{ ID: "sensor", Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveValue := 5 curve := &MockCurve{ ID: "curve", Value: curveValue, } - curves.SpeedCurveMap[curve.GetId()] = curve + curves.RegisterSpeedCurve(curve) fan := &MockFan{ ID: "fan", @@ -556,7 +572,7 @@ func TestFanController_UpdateFanSpeed_FanCurveGaps(t *testing.T) { shouldNeverStop: true, speedCurve: &DutyCycleFan, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) var keys []int for pwm := range DutyCycleFan { @@ -574,19 +590,23 @@ func TestFanController_UpdateFanSpeed_FanCurveGaps(t *testing.T) { 255: 255, } - controller := PidFanController{ + controlLoop := control_loop.NewDirectControlLoop(nil) + + controller := DefaultFanController{ persistence: mockPersistence{}, fan: fan, curve: curve, updateRate: time.Duration(100), + controlLoop: controlLoop, pwmMap: pwmMap, } controller.updateDistinctPwmValues() // WHEN - targetPwm := controller.calculateTargetPwm() + targetPwm, err := controller.calculateTargetPwm() // THEN + assert.NoError(t, err) assert.Equal(t, 54, targetPwm) closestTarget := controller.findClosestDistinctTarget(targetPwm) @@ -603,7 +623,7 @@ func TestFanController_ComputePwmMap_FullRange(t *testing.T) { shouldNeverStop: true, speedCurve: &DutyCycleFan, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) var keys []int for pwm := range DutyCycleFan { @@ -616,7 +636,7 @@ func TestFanController_ComputePwmMap_FullRange(t *testing.T) { expectedPwmMap[i] = i } - controller := PidFanController{ + controller := DefaultFanController{ persistence: mockPersistence{ hasPwmMap: false, }, @@ -644,7 +664,7 @@ func TestFanController_ComputePwmMap_UserOverride(t *testing.T) { speedCurve: &LinearFan, PwmMap: &userDefinedPwmMap, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) var keys []int for pwm := range DutyCycleFan { @@ -657,7 +677,7 @@ func TestFanController_ComputePwmMap_UserOverride(t *testing.T) { expectedPwmMap[i] = i } - controller := PidFanController{ + controller := DefaultFanController{ persistence: mockPersistence{ hasPwmMap: false, }, @@ -685,7 +705,7 @@ func TestFanController_SetPwm(t *testing.T) { shouldNeverStop: true, speedCurve: &LinearFan, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) var keys []int for pwm := range DutyCycleFan { @@ -698,7 +718,7 @@ func TestFanController_SetPwm(t *testing.T) { expectedPwmMap[i] = i } - controller := PidFanController{ + controller := DefaultFanController{ persistence: mockPersistence{ hasPwmMap: false, }, @@ -728,7 +748,7 @@ func TestFanController_SetPwm_UserOverridePwmMap(t *testing.T) { shouldNeverStop: true, speedCurve: &LinearFan, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) var keys []int for pwm := range DutyCycleFan { @@ -736,7 +756,7 @@ func TestFanController_SetPwm_UserOverridePwmMap(t *testing.T) { } sort.Ints(keys) - controller := PidFanController{ + controller := DefaultFanController{ persistence: mockPersistence{ hasPwmMap: false, }, diff --git a/internal/curves/curve.go b/internal/curves/curve.go index 2fa9889..0e5a6d0 100644 --- a/internal/curves/curve.go +++ b/internal/curves/curve.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/markusressel/fan2go/internal/configuration" "github.com/markusressel/fan2go/internal/util" + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/qdm12/reprint" ) type SpeedCurve interface { @@ -14,7 +16,7 @@ type SpeedCurve interface { } var ( - SpeedCurveMap = map[string]SpeedCurve{} + speedCurveMap = cmap.New[SpeedCurve]() ) func NewSpeedCurve(config configuration.CurveConfig) (SpeedCurve, error) { @@ -44,3 +46,18 @@ func NewSpeedCurve(config configuration.CurveConfig) (SpeedCurve, error) { return nil, fmt.Errorf("no matching curve type for curve: %s", config.ID) } + +// RegisterSpeedCurve registers a new speed curve +func RegisterSpeedCurve(curve SpeedCurve) { + speedCurveMap.Set(curve.GetId(), curve) +} + +// GetSpeedCurve returns the speed curve with the given id +func GetSpeedCurve(id string) (SpeedCurve, bool) { + return speedCurveMap.Get(id) +} + +// SnapshotSpeedCurveMap returns a snapshot of the current speed curve map +func SnapshotSpeedCurveMap() map[string]SpeedCurve { + return reprint.This(speedCurveMap.Items()).(map[string]SpeedCurve) +} diff --git a/internal/curves/functional.go b/internal/curves/functional.go index a51c2f4..383e80b 100644 --- a/internal/curves/functional.go +++ b/internal/curves/functional.go @@ -18,7 +18,8 @@ func (c *FunctionSpeedCurve) GetId() string { func (c *FunctionSpeedCurve) Evaluate() (value int, err error) { var curves []SpeedCurve for _, curveId := range c.Config.Function.Curves { - curves = append(curves, SpeedCurveMap[curveId]) + curve, _ := GetSpeedCurve(curveId) + curves = append(curves, curve) } var values []int diff --git a/internal/curves/functional_test.go b/internal/curves/functional_test.go index 854d386..ed921c6 100644 --- a/internal/curves/functional_test.go +++ b/internal/curves/functional_test.go @@ -28,19 +28,19 @@ func TestFunctionCurveSum(t *testing.T) { temp1 := 50000.0 temp2 := 60000.0 - s1 := MockSensor{ + s1 := &MockSensor{ ID: "cpu_sensor", Name: "sensor1", MovingAvg: temp1, } - sensors.SensorMap[s1.GetId()] = &s1 + sensors.RegisterSensor(s1) - s2 := MockSensor{ + s2 := &MockSensor{ ID: "mainboard_sensor", Name: "sensor2", MovingAvg: temp2, } - sensors.SensorMap[s2.GetId()] = &s2 + sensors.RegisterSensor(s2) curve1 := createLinearCurveConfig( "case_fan_front1", @@ -50,7 +50,7 @@ func TestFunctionCurveSum(t *testing.T) { ) c1, _ := NewSpeedCurve(curve1) - SpeedCurveMap[c1.GetId()] = c1 + RegisterSpeedCurve(c1) curve2 := createLinearCurveConfig( "case_fan_back1", @@ -61,7 +61,7 @@ func TestFunctionCurveSum(t *testing.T) { var c2 SpeedCurve c2, _ = NewSpeedCurve(curve2) - SpeedCurveMap[c2.GetId()] = c2 + RegisterSpeedCurve(c2) function := configuration.FunctionSum functionCurveConfig := createFunctionCurveConfig( @@ -73,7 +73,7 @@ func TestFunctionCurveSum(t *testing.T) { }, ) functionCurve, _ := NewSpeedCurve(functionCurveConfig) - SpeedCurveMap[functionCurve.GetId()] = functionCurve + RegisterSpeedCurve(functionCurve) // WHEN result, err := functionCurve.Evaluate() @@ -90,19 +90,19 @@ func TestFunctionCurveDifference(t *testing.T) { temp1 := 60000.0 temp2 := 50000.0 - s1 := MockSensor{ + s1 := &MockSensor{ ID: "cpu_sensor", Name: "sensor1", MovingAvg: temp1, } - sensors.SensorMap[s1.GetId()] = &s1 + sensors.RegisterSensor(s1) - s2 := MockSensor{ + s2 := &MockSensor{ ID: "mainboard_sensor", Name: "sensor2", MovingAvg: temp2, } - sensors.SensorMap[s2.GetId()] = &s2 + sensors.RegisterSensor(s2) curve1 := createLinearCurveConfig( "case_fan_front1", @@ -111,7 +111,7 @@ func TestFunctionCurveDifference(t *testing.T) { 80, ) c1, _ := NewSpeedCurve(curve1) - SpeedCurveMap[c1.GetId()] = c1 + RegisterSpeedCurve(c1) curve2 := createLinearCurveConfig( "case_fan_back1", @@ -120,7 +120,7 @@ func TestFunctionCurveDifference(t *testing.T) { 80, ) c2, _ := NewSpeedCurve(curve2) - SpeedCurveMap[c2.GetId()] = c2 + RegisterSpeedCurve(c2) function := configuration.FunctionDifference functionCurveConfig := createFunctionCurveConfig( @@ -132,7 +132,7 @@ func TestFunctionCurveDifference(t *testing.T) { }, ) functionCurve, _ := NewSpeedCurve(functionCurveConfig) - SpeedCurveMap[functionCurve.GetId()] = functionCurve + RegisterSpeedCurve(functionCurve) // WHEN result, err := functionCurve.Evaluate() @@ -149,19 +149,19 @@ func TestFunctionCurveAverage(t *testing.T) { temp1 := 40000.0 temp2 := 80000.0 - s1 := MockSensor{ + s1 := &MockSensor{ ID: "cpu_sensor", Name: "sensor1", MovingAvg: temp1, } - sensors.SensorMap[s1.GetId()] = &s1 + sensors.RegisterSensor(s1) - s2 := MockSensor{ + s2 := &MockSensor{ ID: "mainboard_sensor", Name: "sensor2", MovingAvg: temp2, } - sensors.SensorMap[s2.GetId()] = &s2 + sensors.RegisterSensor(s2) curve1 := createLinearCurveConfig( "case_fan_front1", @@ -170,7 +170,7 @@ func TestFunctionCurveAverage(t *testing.T) { 80, ) c1, _ := NewSpeedCurve(curve1) - SpeedCurveMap[c1.GetId()] = c1 + RegisterSpeedCurve(c1) curve2 := createLinearCurveConfig( "case_fan_back1", @@ -179,7 +179,7 @@ func TestFunctionCurveAverage(t *testing.T) { 80, ) c2, _ := NewSpeedCurve(curve2) - SpeedCurveMap[c2.GetId()] = c2 + RegisterSpeedCurve(c2) function := configuration.FunctionAverage functionCurveConfig := createFunctionCurveConfig( @@ -191,7 +191,7 @@ func TestFunctionCurveAverage(t *testing.T) { }, ) functionCurve, _ := NewSpeedCurve(functionCurveConfig) - SpeedCurveMap[functionCurve.GetId()] = functionCurve + RegisterSpeedCurve(functionCurve) // WHEN result, err := functionCurve.Evaluate() @@ -208,19 +208,19 @@ func TestFunctionCurveDelta(t *testing.T) { temp1 := 20000.0 temp2 := 40000.0 - s1 := MockSensor{ + s1 := &MockSensor{ ID: "ambient_sensor", Name: "sensor_ambient", MovingAvg: temp1, } - sensors.SensorMap[s1.GetId()] = &s1 + sensors.RegisterSensor(s1) - s2 := MockSensor{ + s2 := &MockSensor{ ID: "water_sensor", Name: "sensor_water", MovingAvg: temp2, } - sensors.SensorMap[s2.GetId()] = &s2 + sensors.RegisterSensor(s2) curve1 := createLinearCurveConfig( "case_fan_front2", @@ -229,7 +229,7 @@ func TestFunctionCurveDelta(t *testing.T) { 60, ) c1, _ := NewSpeedCurve(curve1) - SpeedCurveMap[c1.GetId()] = c1 + RegisterSpeedCurve(c1) curve2 := createLinearCurveConfig( "case_fan_back2", @@ -238,7 +238,7 @@ func TestFunctionCurveDelta(t *testing.T) { 60, ) c2, _ := NewSpeedCurve(curve2) - SpeedCurveMap[c2.GetId()] = c2 + RegisterSpeedCurve(c2) function := configuration.FunctionDelta functionCurveConfig := createFunctionCurveConfig( @@ -250,7 +250,7 @@ func TestFunctionCurveDelta(t *testing.T) { }, ) functionCurve, _ := NewSpeedCurve(functionCurveConfig) - SpeedCurveMap[functionCurve.GetId()] = functionCurve + RegisterSpeedCurve(functionCurve) // WHEN result, err := functionCurve.Evaluate() @@ -267,19 +267,19 @@ func TestFunctionCurveMinimum(t *testing.T) { temp1 := 60000.0 temp2 := 80000.0 - s1 := MockSensor{ + s1 := &MockSensor{ ID: "s1", Name: "sensor1", MovingAvg: temp1, } - sensors.SensorMap[s1.GetId()] = &s1 + sensors.RegisterSensor(s1) - s2 := MockSensor{ + s2 := &MockSensor{ ID: "s2", Name: "sensor2", MovingAvg: temp2, } - sensors.SensorMap[s2.GetId()] = &s2 + sensors.RegisterSensor(s2) curve1 := createLinearCurveConfig( "case_fan_front3", @@ -288,7 +288,7 @@ func TestFunctionCurveMinimum(t *testing.T) { 80, ) c1, _ := NewSpeedCurve(curve1) - SpeedCurveMap[c1.GetId()] = c1 + RegisterSpeedCurve(c1) curve2 := createLinearCurveConfig( "case_fan_back3", @@ -297,7 +297,7 @@ func TestFunctionCurveMinimum(t *testing.T) { 80, ) c2, _ := NewSpeedCurve(curve2) - SpeedCurveMap[c2.GetId()] = c2 + RegisterSpeedCurve(c2) function := configuration.FunctionMinimum functionCurveConfig := createFunctionCurveConfig( @@ -325,19 +325,19 @@ func TestFunctionCurveMaximum(t *testing.T) { temp1 := 40000.0 temp2 := 80000.0 - s1 := MockSensor{ + s1 := &MockSensor{ ID: "s1", Name: "sensor1", MovingAvg: temp1, } - sensors.SensorMap[s1.GetId()] = &s1 + sensors.RegisterSensor(s1) - s2 := MockSensor{ + s2 := &MockSensor{ ID: "s1", Name: "sensor2", MovingAvg: temp2, } - sensors.SensorMap[s2.GetId()] = &s2 + sensors.RegisterSensor(s2) curve1 := createLinearCurveConfig( "case_fan_front4", @@ -346,7 +346,7 @@ func TestFunctionCurveMaximum(t *testing.T) { 80, ) c1, _ := NewSpeedCurve(curve1) - SpeedCurveMap[c1.GetId()] = c1 + RegisterSpeedCurve(c1) curve2 := createLinearCurveConfig( "case_fan_back4", @@ -355,7 +355,7 @@ func TestFunctionCurveMaximum(t *testing.T) { 80, ) c2, _ := NewSpeedCurve(curve2) - SpeedCurveMap[c2.GetId()] = c2 + RegisterSpeedCurve(c2) function := configuration.FunctionMaximum functionCurveConfig := createFunctionCurveConfig( diff --git a/internal/curves/linear.go b/internal/curves/linear.go index 56d9b5d..9496117 100644 --- a/internal/curves/linear.go +++ b/internal/curves/linear.go @@ -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 { @@ -17,7 +19,7 @@ func (c *LinearSpeedCurve) GetId() string { } func (c *LinearSpeedCurve) Evaluate() (value int, err error) { - sensor := sensors.SensorMap[c.Config.Linear.Sensor] + sensor, _ := sensors.GetSensor(c.Config.Linear.Sensor) var avgTemp = sensor.GetMovingAvg() steps := c.Config.Linear.Steps @@ -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 } diff --git a/internal/curves/linear_test.go b/internal/curves/linear_test.go index 7fc20e3..9d7e2d2 100644 --- a/internal/curves/linear_test.go +++ b/internal/curves/linear_test.go @@ -45,11 +45,11 @@ func TestLinearCurveWithMinMax(t *testing.T) { // GIVEN avgTmp := 60000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createLinearCurveConfig( "curve", @@ -72,11 +72,11 @@ func TestLinearCurveWithMinMax(t *testing.T) { func TestLinearCurveWithSteps(t *testing.T) { // GIVEN avgTmp := 60000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createLinearCurveConfigWithSteps( "curve", diff --git a/internal/curves/pid.go b/internal/curves/pid.go index 6674a8b..4430699 100644 --- a/internal/curves/pid.go +++ b/internal/curves/pid.go @@ -19,7 +19,7 @@ func (c *PidSpeedCurve) GetId() string { } func (c *PidSpeedCurve) Evaluate() (value int, err error) { - sensor := sensors.SensorMap[c.Config.PID.Sensor] + sensor, _ := sensors.GetSensor(c.Config.PID.Sensor) var measured float64 measured, err = sensor.GetValue() if err != nil { @@ -31,11 +31,7 @@ func (c *PidSpeedCurve) Evaluate() (value int, err error) { loopValue := c.pidLoop.Loop(pidTarget, measured/1000.0) // clamp to (0..1) - if loopValue > 1 { - loopValue = 1 - } else if loopValue < 0 { - loopValue = 0 - } + loopValue = util.Coerce(loopValue, 0, 1) // map to expected output range curveValue := int(loopValue * 255) diff --git a/internal/curves/pid_test.go b/internal/curves/pid_test.go index 67dcd43..11614ce 100644 --- a/internal/curves/pid_test.go +++ b/internal/curves/pid_test.go @@ -36,11 +36,11 @@ func TestPidCurveProportionalBelowTarget(t *testing.T) { // GIVEN avgTmp := 50000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -70,11 +70,11 @@ func TestPidCurveProportionalAboveTarget(t *testing.T) { // GIVEN avgTmp := 70000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -104,11 +104,11 @@ func TestPidCurveProportionalWayAboveTarget(t *testing.T) { // GIVEN avgTmp := 80000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -140,11 +140,11 @@ func TestPidCurveIntegralBelowTarget(t *testing.T) { // GIVEN avgTmp := 50000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -174,11 +174,11 @@ func TestPidCurveIntegralAboveTarget(t *testing.T) { // GIVEN avgTmp := 70000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -208,11 +208,11 @@ func TestPidCurveIntegralWayAboveTarget(t *testing.T) { // GIVEN avgTmp := 80000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -244,11 +244,11 @@ func TestPidCurveDerivativeNoDiff(t *testing.T) { // GIVEN avgTmp := 60000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -278,11 +278,11 @@ func TestPidCurveDerivativePositiveStaticDiff(t *testing.T) { // GIVEN avgTmp := 60000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -315,11 +315,11 @@ func TestPidCurveDerivativeIncreasingDiff(t *testing.T) { // GIVEN avgTmp := 60000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -354,11 +354,11 @@ func TestPidCurveOnTarget(t *testing.T) { // GIVEN avgTmp := 60000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -392,11 +392,11 @@ func TestPidCurveAboveTarget(t *testing.T) { // GIVEN avgTmp := 70000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", @@ -426,11 +426,11 @@ func TestPidCurveWayAboveTarget(t *testing.T) { // GIVEN avgTmp := 80000.0 - s := MockSensor{ + s := &MockSensor{ Name: "sensor", MovingAvg: avgTmp, } - sensors.SensorMap[s.GetId()] = &s + sensors.RegisterSensor(s) curveConfig := createPidCurveConfig( "curve", diff --git a/internal/fans/cmd.go b/internal/fans/cmd.go index 83086ba..84ba94d 100644 --- a/internal/fans/cmd.go +++ b/internal/fans/cmd.go @@ -115,15 +115,19 @@ func (fan *CmdFan) SetPwm(pwm int) (err error) { return nil } -func (fan *CmdFan) GetFanCurveData() *map[int]float64 { +func (fan *CmdFan) GetFanRpmCurveData() *map[int]float64 { return &interpolated } -func (fan *CmdFan) AttachFanCurveData(curveData *map[int]float64) (err error) { +func (fan *CmdFan) AttachFanRpmCurveData(curveData *map[int]float64) (err error) { // not supported return } +func (fan *CmdFan) UpdateFanRpmCurveValue(pwm int, rpm float64) { + // not supported +} + func (fan *CmdFan) GetCurveId() string { return fan.Config.Curve } diff --git a/internal/fans/cmd_test.go b/internal/fans/cmd_test.go index a4cb2de..cb94f26 100644 --- a/internal/fans/cmd_test.go +++ b/internal/fans/cmd_test.go @@ -344,7 +344,7 @@ func TestCmdFan_GetFanCurveData(t *testing.T) { var interpolated = util.InterpolateLinearly(&map[int]float64{0: 0, 255: 255}, 0, 255) // WHEN - result := fan.GetFanCurveData() + result := fan.GetFanRpmCurveData() // THEN assert.Equal(t, &interpolated, result) @@ -358,7 +358,7 @@ func TestCmdFan_AttachFanCurveData(t *testing.T) { fan, _ := NewFan(config) // WHEN - err := fan.AttachFanCurveData(nil) + err := fan.AttachFanRpmCurveData(nil) // THEN assert.NoError(t, err) diff --git a/internal/fans/common.go b/internal/fans/common.go index 16d5a14..7892f85 100644 --- a/internal/fans/common.go +++ b/internal/fans/common.go @@ -2,8 +2,11 @@ package fans import ( "fmt" - "github.com/markusressel/fan2go/internal/configuration" + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/qdm12/reprint" "sort" + + "github.com/markusressel/fan2go/internal/configuration" ) const ( @@ -30,7 +33,7 @@ const ( ) var ( - FanMap = map[string]Fan{} + fanMap = cmap.New[Fan]() ) type Fan interface { @@ -57,9 +60,12 @@ type Fan interface { GetPwm() (int, error) SetPwm(pwm int) (err error) - // GetFanCurveData returns the fan curve data for this fan - GetFanCurveData() *map[int]float64 - AttachFanCurveData(curveData *map[int]float64) (err error) + // GetFanRpmCurveData returns the fan curve data for this fan + GetFanRpmCurveData() *map[int]float64 + // AttachFanRpmCurveData attaches a complete set of PWM -> RPM mapping values to this fan + AttachFanRpmCurveData(curveData *map[int]float64) (err error) + // UpdateFanRpmCurveValue updates a single PWM -> RPM mapping value + UpdateFanRpmCurveValue(pwm int, rpm float64) // GetCurveId returns the id of the speed curve associated with this fan GetCurveId() string @@ -108,7 +114,7 @@ func ComputePwmBoundaries(fan Fan) (startPwm int, maxPwm int) { userStartPwm := fan.GetStartPwm() startPwm = 255 maxPwm = 255 - pwmRpmMap := fan.GetFanCurveData() + pwmRpmMap := fan.GetFanRpmCurveData() var keys []int for pwm := range *pwmRpmMap { @@ -135,3 +141,18 @@ func ComputePwmBoundaries(fan Fan) (startPwm int, maxPwm int) { return startPwm, maxPwm } + +// RegisterFan registers a new fan +func RegisterFan(fan Fan) { + fanMap.Set(fan.GetId(), fan) +} + +// GetFan returns the fan with the given id +func GetFan(id string) (Fan, bool) { + return fanMap.Get(id) +} + +// SnapshotFanMap returns a snapshot of the current fan map +func SnapshotFanMap() map[string]Fan { + return reprint.This(fanMap.Items()).(map[string]Fan) +} diff --git a/internal/fans/file.go b/internal/fans/file.go index 1c0e825..f85c25f 100644 --- a/internal/fans/file.go +++ b/internal/fans/file.go @@ -116,15 +116,19 @@ func (fan *FileFan) SetPwm(pwm int) (err error) { var interpolated = util.InterpolateLinearly(&map[int]float64{0: 0, 255: 255}, 0, 255) -func (fan *FileFan) GetFanCurveData() *map[int]float64 { +func (fan *FileFan) GetFanRpmCurveData() *map[int]float64 { return &interpolated } -func (fan *FileFan) AttachFanCurveData(curveData *map[int]float64) (err error) { +func (fan *FileFan) AttachFanRpmCurveData(curveData *map[int]float64) (err error) { // not supported return } +func (fan *FileFan) UpdateFanRpmCurveValue(pwm int, rpm float64) { + // not supported +} + func (fan *FileFan) GetCurveId() string { return fan.Config.Curve } diff --git a/internal/fans/file_test.go b/internal/fans/file_test.go index 7f0ae48..886f545 100644 --- a/internal/fans/file_test.go +++ b/internal/fans/file_test.go @@ -336,7 +336,7 @@ func TestFileFan_GetFanCurveData(t *testing.T) { ) // WHEN - result := fan.GetFanCurveData() + result := fan.GetFanRpmCurveData() // THEN assert.Equal(t, expectedFanCurve, *result) diff --git a/internal/fans/hwmon.go b/internal/fans/hwmon.go index 0758b50..2a770fc 100644 --- a/internal/fans/hwmon.go +++ b/internal/fans/hwmon.go @@ -105,7 +105,7 @@ func (fan *HwMonFan) SetPwm(pwm int) (err error) { return err } -func (fan *HwMonFan) GetFanCurveData() *map[int]float64 { +func (fan *HwMonFan) GetFanRpmCurveData() *map[int]float64 { return fan.FanCurveData } @@ -113,7 +113,7 @@ func (fan *HwMonFan) GetFanCurveData() *map[int]float64 { // Note: When the given data is incomplete, all values up until the highest // value in the given dataset will be interpolated linearly // returns os.ErrInvalid if curveData is void of any data -func (fan *HwMonFan) AttachFanCurveData(curveData *map[int]float64) (err error) { +func (fan *HwMonFan) AttachFanRpmCurveData(curveData *map[int]float64) (err error) { if curveData == nil || len(*curveData) <= 0 { ui.Error("Cant attach empty fan curve data to fan %s", fan.GetId()) return os.ErrInvalid @@ -131,6 +131,13 @@ func (fan *HwMonFan) AttachFanCurveData(curveData *map[int]float64) (err error) return err } +func (fan *HwMonFan) UpdateFanRpmCurveValue(pwm int, rpm float64) { + if fan.FanCurveData == nil { + fan.FanCurveData = &map[int]float64{} + } + (*fan.FanCurveData)[pwm] = rpm +} + func (fan *HwMonFan) GetCurveId() string { return fan.Config.Curve } diff --git a/internal/fans/hwmon_test.go b/internal/fans/hwmon_test.go index 6f70ebc..34e12a2 100644 --- a/internal/fans/hwmon_test.go +++ b/internal/fans/hwmon_test.go @@ -305,11 +305,11 @@ func TestHwMonFan_AttachFanCurveData(t *testing.T) { } // WHEN - err := fan.AttachFanCurveData(&interpolated) + err := fan.AttachFanRpmCurveData(&interpolated) // THEN assert.NoError(t, err) - assert.Equal(t, &interpolated, fan.GetFanCurveData()) + assert.Equal(t, &interpolated, fan.GetFanRpmCurveData()) assert.Equal(t, 10, fan.GetMinPwm()) assert.Equal(t, 10, fan.GetStartPwm()) assert.Equal(t, 200, fan.GetMaxPwm()) diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go index 6dec653..ee0d973 100644 --- a/internal/persistence/persistence.go +++ b/internal/persistence/persistence.go @@ -75,7 +75,7 @@ func (p persistence) SaveFanPwmData(fan fans.Fan) (err error) { // convert the curve data moving window to a map to arrays, so we can persist them fanCurveDataMap := map[int]float64{} - for key, value := range *fan.GetFanCurveData() { + for key, value := range *fan.GetFanRpmCurveData() { fanCurveDataMap[key] = value } diff --git a/internal/persistence/persistence_test.go b/internal/persistence/persistence_test.go index 4f26e09..3bed284 100644 --- a/internal/persistence/persistence_test.go +++ b/internal/persistence/persistence_test.go @@ -143,9 +143,9 @@ func createFan(neverStop bool, curveData map[int]float64) (fan fans.Fan, err err Curve: "curve", }, } - fans.FanMap[fan.GetId()] = fan + fans.RegisterFan(fan) - err = fan.AttachFanCurveData(&curveData) + err = fan.AttachFanRpmCurveData(&curveData) return fan, err } diff --git a/internal/sensors/cmd.go b/internal/sensors/cmd.go index a12e72d..ba2fdfd 100644 --- a/internal/sensors/cmd.go +++ b/internal/sensors/cmd.go @@ -6,6 +6,7 @@ import ( "github.com/markusressel/fan2go/internal/ui" "github.com/markusressel/fan2go/internal/util" "strconv" + "sync" "time" ) @@ -13,17 +14,19 @@ type CmdSensor struct { Name string `json:"name"` Config configuration.SensorConfig `json:"configuration"` MovingAvg float64 `json:"movingAvg"` + + mu sync.Mutex } -func (sensor CmdSensor) GetId() string { +func (sensor *CmdSensor) GetId() string { return sensor.Config.ID } -func (sensor CmdSensor) GetConfig() configuration.SensorConfig { +func (sensor *CmdSensor) GetConfig() configuration.SensorConfig { return sensor.Config } -func (sensor CmdSensor) GetValue() (float64, error) { +func (sensor *CmdSensor) GetValue() (float64, error) { timeout := 2 * time.Second exec := sensor.Config.Cmd.Exec args := sensor.Config.Cmd.Args @@ -41,10 +44,14 @@ func (sensor CmdSensor) GetValue() (float64, error) { return temp, nil } -func (sensor CmdSensor) GetMovingAvg() (avg float64) { +func (sensor *CmdSensor) GetMovingAvg() (avg float64) { + sensor.mu.Lock() + defer sensor.mu.Unlock() return sensor.MovingAvg } func (sensor *CmdSensor) SetMovingAvg(avg float64) { + sensor.mu.Lock() + defer sensor.mu.Unlock() sensor.MovingAvg = avg } diff --git a/internal/sensors/common.go b/internal/sensors/common.go index c368b22..c463730 100644 --- a/internal/sensors/common.go +++ b/internal/sensors/common.go @@ -3,10 +3,13 @@ package sensors import ( "fmt" "github.com/markusressel/fan2go/internal/configuration" + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/qdm12/reprint" + "sync" ) var ( - SensorMap = map[string]Sensor{} + sensorMap = cmap.New[Sensor]() ) type Sensor interface { @@ -28,20 +31,41 @@ func NewSensor(config configuration.SensorConfig) (Sensor, error) { Index: config.HwMon.Index, Input: config.HwMon.TempInput, Config: config, + + mu: sync.Mutex{}, }, nil } if config.File != nil { return &FileSensor{ Config: config, + + mu: sync.Mutex{}, }, nil } if config.Cmd != nil { return &CmdSensor{ Config: config, + + mu: sync.Mutex{}, }, nil } return nil, fmt.Errorf("no matching sensor type for sensor: %s", config.ID) } + +// RegisterSensor registers a new sensor +func RegisterSensor(sensor Sensor) { + sensorMap.Set(sensor.GetId(), sensor) +} + +// GetSensor returns the sensor with the given id +func GetSensor(id string) (Sensor, bool) { + return sensorMap.Get(id) +} + +// SnapshotSensorMap returns a snapshot of the current sensor map +func SnapshotSensorMap() map[string]Sensor { + return reprint.This(sensorMap.Items()).(map[string]Sensor) +} diff --git a/internal/sensors/file.go b/internal/sensors/file.go index 7a98d60..bdebd55 100644 --- a/internal/sensors/file.go +++ b/internal/sensors/file.go @@ -7,22 +7,25 @@ import ( "os/user" "path/filepath" "strings" + "sync" ) type FileSensor struct { Config configuration.SensorConfig `json:"configuration"` MovingAvg float64 `json:"movingAvg"` + + mu sync.Mutex } -func (sensor FileSensor) GetId() string { +func (sensor *FileSensor) GetId() string { return sensor.Config.ID } -func (sensor FileSensor) GetConfig() configuration.SensorConfig { +func (sensor *FileSensor) GetConfig() configuration.SensorConfig { return sensor.Config } -func (sensor FileSensor) GetValue() (float64, error) { +func (sensor *FileSensor) GetValue() (float64, error) { filePath := sensor.Config.File.Path // resolve home dir path if strings.HasPrefix(filePath, "~") { @@ -44,10 +47,14 @@ func (sensor FileSensor) GetValue() (float64, error) { return result, nil } -func (sensor FileSensor) GetMovingAvg() (avg float64) { +func (sensor *FileSensor) GetMovingAvg() (avg float64) { + sensor.mu.Lock() + defer sensor.mu.Unlock() return sensor.MovingAvg } func (sensor *FileSensor) SetMovingAvg(avg float64) { + sensor.mu.Lock() + defer sensor.mu.Unlock() sensor.MovingAvg = avg } diff --git a/internal/sensors/hwmon.go b/internal/sensors/hwmon.go index 4c354fa..e150c6e 100644 --- a/internal/sensors/hwmon.go +++ b/internal/sensors/hwmon.go @@ -3,6 +3,7 @@ package sensors import ( "github.com/markusressel/fan2go/internal/configuration" "github.com/markusressel/fan2go/internal/util" + "sync" ) type HwmonSensor struct { @@ -13,17 +14,19 @@ type HwmonSensor struct { Min int `json:"min"` Config configuration.SensorConfig `json:"configuration"` MovingAvg float64 `json:"movingAvg"` + + mu sync.Mutex } -func (sensor HwmonSensor) GetId() string { +func (sensor *HwmonSensor) GetId() string { return sensor.Config.ID } -func (sensor HwmonSensor) GetConfig() configuration.SensorConfig { +func (sensor *HwmonSensor) GetConfig() configuration.SensorConfig { return sensor.Config } -func (sensor HwmonSensor) GetValue() (result float64, err error) { +func (sensor *HwmonSensor) GetValue() (result float64, err error) { integer, err := util.ReadIntFromFile(sensor.Input) if err != nil { return 0, err @@ -32,10 +35,14 @@ func (sensor HwmonSensor) GetValue() (result float64, err error) { return result, err } -func (sensor HwmonSensor) GetMovingAvg() (avg float64) { +func (sensor *HwmonSensor) GetMovingAvg() (avg float64) { + sensor.mu.Lock() + defer sensor.mu.Unlock() return sensor.MovingAvg } func (sensor *HwmonSensor) SetMovingAvg(avg float64) { + sensor.mu.Lock() + defer sensor.mu.Unlock() sensor.MovingAvg = avg } diff --git a/internal/sensors/sensors_test.go b/internal/sensors/sensors_test.go index 8b73d6d..1206a57 100644 --- a/internal/sensors/sensors_test.go +++ b/internal/sensors/sensors_test.go @@ -16,6 +16,6 @@ func CreateSensor( }, MovingAvg: avgTmp, } - SensorMap[sensor.GetId()] = sensor + RegisterSensor(sensor) return sensor } diff --git a/internal/statistics/controller.go b/internal/statistics/controller.go index 4b78e83..d6097bd 100644 --- a/internal/statistics/controller.go +++ b/internal/statistics/controller.go @@ -42,7 +42,7 @@ func (collector *ControllerCollector) Describe(ch chan<- *prometheus.Desc) { func (collector *ControllerCollector) Collect(ch chan<- prometheus.Metric) { for _, contr := range collector.controllers { switch contr.(type) { - case *controller.PidFanController: + case *controller.DefaultFanController: fanId := contr.GetFanId() ch <- prometheus.MustNewConstMetric(collector.unexpectedPwmValueCount, prometheus.CounterValue, float64(contr.GetStatistics().UnexpectedPwmValueCount), fanId) ch <- prometheus.MustNewConstMetric(collector.increasedMinPwmCount, prometheus.CounterValue, float64(contr.GetStatistics().IncreasedMinPwmCount), fanId) diff --git a/internal/ui/logging.go b/internal/ui/logging.go index e016981..9e50424 100644 --- a/internal/ui/logging.go +++ b/internal/ui/logging.go @@ -10,10 +10,18 @@ func SetDebugEnabled(enabled bool) { pterm.PrintDebugMessages = enabled } +func Print(format string) { + pterm.Print(format) +} + func Printf(format string, a ...interface{}) { pterm.Printf(format, a...) } +func Println(format string) { + pterm.Println(format) +} + func Printfln(format string, a ...interface{}) { pterm.Printfln(format, a...) }