diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 3bd8a60..0bee1db 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -56,6 +56,7 @@ type PidFanController struct { // the original pwm_enabled flag state of the fan before starting the controller originalPwmEnabled fans.ControlMode // the original pwm value of the fan before starting the controller + // Note: this is the raw value read from the fan, no pwmMap is applied to it originalPwmValue int // the last pwm value that was set to the fan, **before** applying the pwmMap to it lastSetPwm *int @@ -82,7 +83,7 @@ func NewFanController( curve: curves.SpeedCurveMap[fan.GetCurveId()], updateRate: updateRate, pwmValuesWithDistinctTarget: []int{}, - pwmMap: map[int]int{}, + pwmMap: nil, pidLoop: &pidLoop, minPwmOffset: 0, } @@ -301,7 +302,7 @@ func (f *PidFanController) RunInitializationSequence() (err error) { ui.Error("Unable to run initialization sequence on %s: %v", fan.GetId(), err) return err } - expectedPwm := f.pwmMap[pwm] + expectedPwm := f.applyPwmMapping(pwm) time.Sleep(pwmSetGetDelay) actualPwm, err := fan.GetPwm() if err != nil { @@ -386,7 +387,7 @@ func trySetManualPwm(fan fans.Fan) error { func (f *PidFanController) restorePwmEnabled() { ui.Info("Trying to restore fan settings for %s...", f.fan.GetId()) - err := f.setPwm(f.originalPwmValue) + err := f.fan.SetPwm(f.originalPwmValue) if err != nil { ui.Warning("Error restoring original PWM value for fan %s: %v", f.fan.GetId(), err) } @@ -399,7 +400,7 @@ func (f *PidFanController) restorePwmEnabled() { } } // if this fails, try to set it to max speed instead - err = f.setPwm(fans.MaxPwmValue) + err = f.fan.SetPwm(fans.MaxPwmValue) if err != nil { ui.Warning("Unable to restore fan %s, make sure it is running!", f.fan.GetId()) } @@ -432,7 +433,7 @@ func (f *PidFanController) calculateTargetPwm() int { if f.lastSetPwm != nil && f.pwmMap != nil { lastSetPwm := *(f.lastSetPwm) - expected := f.pwmMap[f.findClosestDistinctTarget(lastSetPwm)] + expected := f.applyPwmMapping(f.findClosestDistinctTarget(lastSetPwm)) if currentPwm, err := fan.GetPwm(); err == nil { if currentPwm != expected { f.stats.UnexpectedPwmValueCount += 1 @@ -475,16 +476,15 @@ func (f *PidFanController) setPwm(target int) (err error) { current, err := f.fan.GetPwm() closestTarget := f.findClosestDistinctTarget(target) - closestExpected := f.pwmMap[closestTarget] + closestExpected := f.applyPwmMapping(closestTarget) f.lastSetPwm = &target - if err == nil { - if closestExpected == current { - // nothing to do - return nil - } + if err == nil && closestExpected == current { + // nothing to do + return nil + } else { + return f.fan.SetPwm(closestExpected) } - return f.fan.SetPwm(closestTarget) } func (f *PidFanController) waitForFanToSettle(fan fans.Fan) { @@ -511,6 +511,12 @@ func (f *PidFanController) waitForFanToSettle(fan fans.Fan) { ui.Debug("Fan %s has settled (current RPM max diff: %f)", fan.GetId(), measuredRpmDiffMax) } +// findClosestDistinctTarget traverses the entries of the pwmMap and returns +// the internal pwm value (key) of the entry whose value is closest (and distinct) value +// to the requested [target] value. +// +// 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 { return util.FindClosest(target, f.pwmValuesWithDistinctTarget) } @@ -551,14 +557,17 @@ func (f *PidFanController) computePwmMap() (err error) { return nil } - f.pwmMap, err = f.persistence.LoadFanPwmMap(f.fan.GetId()) + savedPwmMap, err := f.persistence.LoadFanPwmMap(f.fan.GetId()) if err == nil && f.pwmMap != nil { ui.Info("FanController: Using saved value for pwm map of Fan '%s'", f.fan.GetId()) + f.pwmMap = savedPwmMap return nil } - ui.Info("Computing pwm map...") - f.computePwmMapAutomatically() + if f.pwmMap == nil { + ui.Info("Computing pwm map...") + f.computePwmMapAutomatically() + } ui.Debug("Saving pwm map to fan...") return f.persistence.SaveFanPwmMap(f.fan.GetId(), f.pwmMap) @@ -581,7 +590,7 @@ func (f *PidFanController) computePwmMapAutomatically() { } f.pwmMap = pwmMap - _ = fan.SetPwm(f.pwmMap[fan.GetStartPwm()]) + _ = fan.SetPwm(f.applyPwmMapping(fan.GetStartPwm())) } func (f *PidFanController) updateDistinctPwmValues() { @@ -597,3 +606,7 @@ func (f *PidFanController) increaseMinPwmOffset() { f.stats.MinPwmOffset = f.minPwmOffset f.stats.IncreasedMinPwmCount += 1 } + +func (f *PidFanController) applyPwmMapping(target int) int { + return f.pwmMap[target] +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index d6dc394..149552c 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -66,6 +66,7 @@ type MockFan struct { curveId string shouldNeverStop bool speedCurve *map[int]float64 + PwmMap *map[int]int } func (fan MockFan) GetStartPwm() int { @@ -156,6 +157,109 @@ func (fan MockFan) Supports(feature fans.FeatureFlag) bool { } var ( + PwmMapForFanWithLimitedRange = map[int]int{ + 0: 0, + 3: 1, + 5: 2, + 8: 3, + 10: 4, + 13: 5, + 15: 6, + 18: 7, + 20: 8, + 23: 9, + 25: 10, + 28: 11, + 31: 12, + 33: 13, + 36: 14, + 38: 15, + 41: 16, + 43: 17, + 46: 18, + 48: 19, + 51: 20, + 54: 21, + 56: 22, + 59: 23, + 61: 24, + 64: 25, + 66: 26, + 69: 27, + 71: 28, + 74: 29, + 77: 30, + 79: 31, + 82: 32, + 85: 33, + 87: 34, + 90: 35, + 92: 36, + 95: 37, + 97: 38, + 100: 39, + 103: 40, + 105: 41, + 108: 42, + 110: 43, + 113: 44, + 116: 45, + 118: 46, + 121: 47, + 123: 48, + 126: 49, + 128: 50, + 131: 51, + 134: 52, + 136: 53, + 139: 54, + 141: 55, + 144: 56, + 147: 57, + 149: 58, + 152: 59, + 154: 60, + 157: 61, + 160: 62, + 162: 63, + 165: 64, + 167: 65, + 170: 66, + 172: 67, + 175: 68, + 178: 69, + 180: 70, + 183: 71, + 185: 72, + 188: 73, + 190: 74, + 193: 75, + 196: 76, + 198: 77, + 201: 78, + 203: 79, + 206: 80, + 208: 81, + 211: 82, + 214: 83, + 216: 84, + 219: 85, + 221: 86, + 224: 87, + 226: 88, + 229: 89, + 232: 90, + 234: 91, + 237: 92, + 239: 93, + 242: 94, + 244: 95, + 247: 96, + 250: 97, + 252: 98, + 255: 100, + } + LinearFan = util.InterpolateLinearly( &map[int]float64{ 0: 0.0, @@ -491,30 +595,54 @@ func TestFanController_UpdateFanSpeed_FanCurveGaps(t *testing.T) { func TestFanController_ComputePwmMap_FullRange(t *testing.T) { // GIVEN - avgTmp := 40000.0 + fan := &MockFan{ + ID: "fan", + PWM: 0, + RPM: 100, + MinPWM: 50, + shouldNeverStop: true, + speedCurve: &DutyCycleFan, + } + fans.FanMap[fan.GetId()] = fan - s := MockSensor{ - ID: "sensor", - Name: "sensor", - MovingAvg: avgTmp, + var keys []int + for pwm := range DutyCycleFan { + keys = append(keys, pwm) } - sensors.SensorMap[s.GetId()] = &s + sort.Ints(keys) - curveValue := 5 - curve := &MockCurve{ - ID: "curve", - Value: curveValue, + expectedPwmMap := map[int]int{} + for i := 0; i <= 255; i++ { + expectedPwmMap[i] = i } - curves.SpeedCurveMap[curve.GetId()] = curve + controller := PidFanController{ + persistence: mockPersistence{ + hasPwmMap: false, + }, + fan: fan, + updateRate: time.Duration(100), + } + + // WHEN + err := controller.computePwmMap() + + // THEN + assert.NoError(t, err) + assert.Equal(t, expectedPwmMap, controller.pwmMap) +} + +func TestFanController_ComputePwmMap_UserOverride(t *testing.T) { + // GIVEN + userDefinedPwmMap := PwmMapForFanWithLimitedRange fan := &MockFan{ ID: "fan", PWM: 0, RPM: 100, MinPWM: 50, - curveId: curve.GetId(), shouldNeverStop: true, - speedCurve: &DutyCycleFan, + speedCurve: &LinearFan, + PwmMap: &userDefinedPwmMap, } fans.FanMap[fan.GetId()] = fan @@ -534,9 +662,8 @@ func TestFanController_ComputePwmMap_FullRange(t *testing.T) { hasPwmMap: false, }, fan: fan, - curve: curve, updateRate: time.Duration(100), - pwmMap: map[int]int{}, + pwmMap: userDefinedPwmMap, } controller.updateDistinctPwmValues() @@ -545,5 +672,86 @@ func TestFanController_ComputePwmMap_FullRange(t *testing.T) { // THEN assert.NoError(t, err) - assert.Equal(t, expectedPwmMap, controller.pwmMap) + assert.Equal(t, userDefinedPwmMap, controller.pwmMap) +} + +func TestFanController_SetPwm(t *testing.T) { + // GIVEN + fan := &MockFan{ + ID: "fan", + PWM: 0, + RPM: 100, + MinPWM: 50, + shouldNeverStop: true, + speedCurve: &LinearFan, + } + fans.FanMap[fan.GetId()] = fan + + var keys []int + for pwm := range DutyCycleFan { + keys = append(keys, pwm) + } + sort.Ints(keys) + + expectedPwmMap := map[int]int{} + for i := 0; i <= 255; i++ { + expectedPwmMap[i] = i + } + + controller := PidFanController{ + persistence: mockPersistence{ + hasPwmMap: false, + }, + fan: fan, + updateRate: time.Duration(100), + pwmMap: expectedPwmMap, + } + err := controller.computePwmMap() + assert.NoError(t, err) + controller.updateDistinctPwmValues() + + // WHEN + err = controller.setPwm(100) + + // THEN + assert.NoError(t, err) + assert.Equal(t, 100, fan.PWM) +} + +func TestFanController_SetPwm_UserOverridePwmMap(t *testing.T) { + // GIVEN + fan := &MockFan{ + ID: "fan", + PWM: 0, + RPM: 100, + MinPWM: 50, + shouldNeverStop: true, + speedCurve: &LinearFan, + } + fans.FanMap[fan.GetId()] = fan + + var keys []int + for pwm := range DutyCycleFan { + keys = append(keys, pwm) + } + sort.Ints(keys) + + controller := PidFanController{ + persistence: mockPersistence{ + hasPwmMap: false, + }, + fan: fan, + updateRate: time.Duration(100), + pwmMap: PwmMapForFanWithLimitedRange, + } + err := controller.computePwmMap() + assert.NoError(t, err) + controller.updateDistinctPwmValues() + + // WHEN + err = controller.setPwm(100) + + // THEN + assert.NoError(t, err) + assert.Equal(t, 39, fan.PWM) }