11package controller
22
33import (
4+ "context"
45 "fmt"
56 "math"
67 "sort"
@@ -14,6 +15,15 @@ import (
1415
1516const pwmMismatchRetries = 2
1617
18+ func sleepWithContext (ctx context.Context , d time.Duration ) error {
19+ select {
20+ case <- ctx .Done ():
21+ return ctx .Err ()
22+ case <- time .After (d ):
23+ return nil
24+ }
25+ }
26+
1727type FanCurveAnalyzer struct {
1828 fanController * DefaultFanController
1929}
@@ -26,7 +36,7 @@ func NewFanCurveAnalyzer(
2636 }
2737}
2838
29- func (f * FanCurveAnalyzer ) RunInitializationSequence () (rpmCurve map [int ]float64 , err error ) {
39+ func (f * FanCurveAnalyzer ) RunInitializationSequence (ctx context. Context ) (rpmCurve map [int ]float64 , err error ) {
3040 fan := f .fanController .fan
3141
3242 if ! fan .Supports (fans .FeatureRpmSensor ) {
@@ -51,21 +61,21 @@ func (f *FanCurveAnalyzer) RunInitializationSequence() (rpmCurve map[int]float64
5161 initStarted := time .Now ()
5262
5363 startBoundaryStarted := time .Now ()
54- startIdx , _ , err := f .detectStartBoundary (fan , distinct , curveData )
64+ startIdx , _ , err := f .detectStartBoundary (ctx , fan , distinct , curveData )
5565 if err != nil {
5666 return nil , err
5767 }
5868 ui .Info ("Fan %s: Boundary discovery (start) finished in %s" , fan .GetId (), time .Since (startBoundaryStarted ).Round (time .Millisecond ))
5969
6070 maxBoundaryStarted := time .Now ()
61- maxIdx , _ , err := f .detectMaxBoundary (fan , distinct , startIdx , curveData )
71+ maxIdx , _ , err := f .detectMaxBoundary (ctx , fan , distinct , startIdx , curveData )
6272 if err != nil {
6373 return nil , err
6474 }
6575 ui .Info ("Fan %s: Boundary discovery (max) finished in %s" , fan .GetId (), time .Since (maxBoundaryStarted ).Round (time .Millisecond ))
6676
6777 interiorStarted := time .Now ()
68- curveData , err = f .sampleInteriorCoarse (fan , distinct , startIdx , maxIdx , curveData )
78+ curveData , err = f .sampleInteriorCoarse (ctx , fan , distinct , startIdx , maxIdx , curveData )
6979 if err != nil {
7080 return nil , err
7181 }
@@ -82,6 +92,7 @@ func (f *FanCurveAnalyzer) RunInitializationSequence() (rpmCurve map[int]float64
8292}
8393
8494func (f * FanCurveAnalyzer ) detectStartBoundary (
95+ ctx context.Context ,
8596 fan fans.Fan ,
8697 distinct []int ,
8798 curveData map [int ]float64 ,
@@ -95,7 +106,7 @@ func (f *FanCurveAnalyzer) detectStartBoundary(
95106 for low <= high {
96107 mid := (low + high ) / 2
97108 pwm := distinct [mid ]
98- rpm , measureErr := f .measureAtPwm (fan , pwm , configuration .CurrentConfig .Analysis .SettleTimeout )
109+ rpm , measureErr := f .measureAtPwm (ctx , fan , pwm , configuration .CurrentConfig .Analysis .SettleTimeout )
99110 if measureErr != nil {
100111 return 0 , 0 , measureErr
101112 }
@@ -134,6 +145,7 @@ func (f *FanCurveAnalyzer) detectStartBoundary(
134145}
135146
136147func (f * FanCurveAnalyzer ) detectMaxBoundary (
148+ ctx context.Context ,
137149 fan fans.Fan ,
138150 distinct []int ,
139151 startIdx int ,
@@ -162,7 +174,7 @@ func (f *FanCurveAnalyzer) detectMaxBoundary(
162174 ui .Info ("Fan %s: Discovering max boundary..." , fan .GetId ())
163175 for idx := lastIdx ; idx >= topStartIdx ; idx -= step {
164176 pwm := distinct [idx ]
165- rpm , measureErr := f .measureAtPwm (fan , pwm , 0 )
177+ rpm , measureErr := f .measureAtPwm (ctx , fan , pwm , 0 )
166178 if measureErr != nil {
167179 return 0 , 0 , measureErr
168180 }
@@ -186,7 +198,7 @@ func (f *FanCurveAnalyzer) detectMaxBoundary(
186198 pwm := distinct [idx ]
187199 rpm , exists := fastScan [pwm ]
188200 if ! exists {
189- measureRpm , measureErr := f .measureAtPwm (fan , pwm , 0 )
201+ measureRpm , measureErr := f .measureAtPwm (ctx , fan , pwm , 0 )
190202 if measureErr != nil {
191203 return 0 , 0 , measureErr
192204 }
@@ -204,7 +216,7 @@ func (f *FanCurveAnalyzer) detectMaxBoundary(
204216 }
205217
206218 if roughIdx >= 0 {
207- confirmedIdx , confirmedPwm , confirmErr := f .confirmMaxBoundary (fan , distinct , startIdx , roughIdx , curveData )
219+ confirmedIdx , confirmedPwm , confirmErr := f .confirmMaxBoundary (ctx , fan , distinct , startIdx , roughIdx , curveData )
208220 if confirmErr != nil {
209221 return 0 , 0 , confirmErr
210222 }
@@ -219,6 +231,7 @@ func (f *FanCurveAnalyzer) detectMaxBoundary(
219231}
220232
221233func (f * FanCurveAnalyzer ) confirmMaxBoundary (
234+ ctx context.Context ,
222235 fan fans.Fan ,
223236 distinct []int ,
224237 startIdx int ,
@@ -246,7 +259,7 @@ func (f *FanCurveAnalyzer) confirmMaxBoundary(
246259 peakConfirmed := 0.0
247260 for _ , idx := range confirmIndices {
248261 pwm := distinct [idx ]
249- rpm , measureErr := f .measureAtPwm (fan , pwm , configuration .CurrentConfig .Analysis .SettleTimeout )
262+ rpm , measureErr := f .measureAtPwm (ctx , fan , pwm , configuration .CurrentConfig .Analysis .SettleTimeout )
250263 if measureErr != nil {
251264 return 0 , 0 , measureErr
252265 }
@@ -284,6 +297,7 @@ func (f *FanCurveAnalyzer) confirmMaxBoundary(
284297}
285298
286299func (f * FanCurveAnalyzer ) sampleInteriorCoarse (
300+ ctx context.Context ,
287301 fan fans.Fan ,
288302 distinct []int ,
289303 startIdx int ,
@@ -308,7 +322,7 @@ func (f *FanCurveAnalyzer) sampleInteriorCoarse(
308322 continue
309323 }
310324 settleTimeout := settleTimeoutForPwmJump (lastMeasuredPwm , pwm , configuration .CurrentConfig .Analysis .SettleTimeout )
311- rpm , err := f .measureAtPwm (fan , pwm , settleTimeout )
325+ rpm , err := f .measureAtPwm (ctx , fan , pwm , settleTimeout )
312326 if err != nil {
313327 return curveData , err
314328 }
@@ -394,13 +408,15 @@ func (f *FanCurveAnalyzer) rpmCurveMeasurementCleanup(curveData map[int]float64)
394408// expected value after setting it even after waiting FanResponseDelay (indicates the hardware ignored the request).
395409// If settleTimeout > 0, waitForFanToSettle is called with that timeout (used for large PWM steps).
396410// If settleTimeout == 0, a plain FanResponseDelay sleep is used instead (sufficient for small steps).
397- func (f * FanCurveAnalyzer ) measureAtPwm (fan fans.Fan , pwm int , settleTimeout time.Duration ) (float64 , error ) {
411+ func (f * FanCurveAnalyzer ) measureAtPwm (ctx context. Context , fan fans.Fan , pwm int , settleTimeout time.Duration ) (float64 , error ) {
398412 actualPwm := f .fanController .applyPwmMapToTarget (pwm )
399413 matchedPwm := false
400414 for attempt := 0 ; attempt <= pwmMismatchRetries ; attempt ++ {
401415 if attempt > 0 {
402416 // The reported PWM may lag behind the requested value on some hardware, so wait FanResponseDelay before retrying.
403- time .Sleep (time .Duration (configuration .CurrentConfig .FanResponseDelay ) * time .Second )
417+ if err := sleepWithContext (ctx , time .Duration (configuration .CurrentConfig .FanResponseDelay )* time .Second ); err != nil {
418+ return 0 , err
419+ }
404420 }
405421 err := f .fanController .setPwm (actualPwm )
406422 if err != nil {
@@ -427,10 +443,14 @@ func (f *FanCurveAnalyzer) measureAtPwm(fan fans.Fan, pwm int, settleTimeout tim
427443 }
428444
429445 if settleTimeout > 0 {
430- f .waitForFanToSettle (fan , settleTimeout )
446+ if err := f .waitForFanToSettle (ctx , fan , settleTimeout ); err != nil {
447+ return 0 , err
448+ }
431449 } else {
432450 // Small PWM step — a response-delay sleep is sufficient.
433- time .Sleep (time .Duration (configuration .CurrentConfig .FanResponseDelay ) * time .Second )
451+ if err := sleepWithContext (ctx , time .Duration (configuration .CurrentConfig .FanResponseDelay )* time .Second ); err != nil {
452+ return 0 , err
453+ }
434454 }
435455
436456 sampleCount := configuration .CurrentConfig .Analysis .SampleCount
@@ -441,7 +461,9 @@ func (f *FanCurveAnalyzer) measureAtPwm(fan fans.Fan, pwm int, settleTimeout tim
441461 samples := make ([]float64 , 0 , sampleCount )
442462 for i := 0 ; i < sampleCount ; i ++ {
443463 if i > 0 {
444- time .Sleep (sampleDelay )
464+ if err := sleepWithContext (ctx , sampleDelay ); err != nil {
465+ return 0 , err
466+ }
445467 }
446468 r , err := fan .GetRpm ()
447469 if err != nil {
@@ -459,7 +481,7 @@ func (f *FanCurveAnalyzer) measureAtPwm(fan fans.Fan, pwm int, settleTimeout tim
459481// waitForFanToSettle waits until the fan's RPM readings are stable (requiredConsecutive consecutive
460482// readings with diff <= MaxRpmDiffForSettledFan). If timeout > 0 and the deadline is exceeded, a
461483// warning is logged and the function returns early rather than blocking forever.
462- func (f * FanCurveAnalyzer ) waitForFanToSettle (fan fans.Fan , timeout time.Duration ) {
484+ func (f * FanCurveAnalyzer ) waitForFanToSettle (ctx context. Context , fan fans.Fan , timeout time.Duration ) error {
463485 const requiredConsecutive = 3
464486 const settleSampleInterval = 500 * time .Millisecond
465487 const settleWindowSize = 8
@@ -484,9 +506,11 @@ func (f *FanCurveAnalyzer) waitForFanToSettle(fan fans.Fan, timeout time.Duratio
484506 for consecutiveStable < requiredConsecutive {
485507 if timeout > 0 && time .Now ().After (deadline ) {
486508 ui .Warning ("Fan %s did not settle within %v, continuing anyway (%d/%d stable readings)" , fan .GetId (), timeout , consecutiveStable , requiredConsecutive )
487- return
509+ return nil
510+ }
511+ if err := sleepWithContext (ctx , settleSampleInterval ); err != nil {
512+ return err
488513 }
489- time .Sleep (settleSampleInterval )
490514
491515 currentRpm , err := fan .GetRpm ()
492516 if err != nil {
@@ -519,6 +543,7 @@ func (f *FanCurveAnalyzer) waitForFanToSettle(fan fans.Fan, timeout time.Duratio
519543 fan .GetId (), consecutiveStable , requiredConsecutive , meanNow , rangeNow )
520544 }
521545 ui .Debug ("Fan %s has settled (%d consecutive stable readings)" , fan .GetId (), requiredConsecutive )
546+ return nil
522547}
523548
524549func evaluateAdaptiveSettling (window []float64 , prevMean * float64 , prevRange * float64 , baseThreshold float64 ) (bool , float64 , float64 ) {
0 commit comments