日常更新
This commit is contained in:
@@ -1,296 +1,296 @@
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
/// <summary>
|
||||
/// Background loop: LHM temp → curve → ectool fanduty; ectool RPM; fail-safe handling.
|
||||
/// </summary>
|
||||
public sealed class FanController : IDisposable
|
||||
{
|
||||
private readonly object _cfgLock = new();
|
||||
private AppConfig _config;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
private CpuTempReader? _cpu;
|
||||
private volatile bool _paused;
|
||||
private int _consecutiveTempErrors;
|
||||
private int _cycleIndex;
|
||||
private bool _failSafeActive;
|
||||
private byte? _lastCommandedDuty;
|
||||
private byte _rampStartDuty;
|
||||
private byte _rampTargetDuty;
|
||||
private int _rampStepIndex;
|
||||
|
||||
public FanController(AppConfig initialConfig)
|
||||
{
|
||||
_config = initialConfig;
|
||||
}
|
||||
|
||||
public event EventHandler<FanSampleEventArgs>? Sample;
|
||||
|
||||
public bool Paused
|
||||
{
|
||||
get => _paused;
|
||||
set => _paused = value;
|
||||
}
|
||||
|
||||
public void UpdateConfig(AppConfig config)
|
||||
{
|
||||
lock (_cfgLock)
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
_cpu = new CpuTempReader();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
_loop = Task.Run(() => LoopAsync(token), token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
|
||||
private void StopInternal(bool waitForLoop)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
|
||||
if (waitForLoop && _loop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_loop.Wait(8000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
}
|
||||
|
||||
_cpu?.Dispose();
|
||||
_cpu = null;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_loop = null;
|
||||
}
|
||||
|
||||
public async Task RestoreAutoFanAsync()
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
if (!File.Exists(cfg.EctoolPath))
|
||||
return;
|
||||
|
||||
var (ok, _, err) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs)
|
||||
.ConfigureAwait(false);
|
||||
if (!ok)
|
||||
DebugLog($"autofanctrl: {err}");
|
||||
}
|
||||
|
||||
private async Task LoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
var mode = string.Equals(cfg.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase)
|
||||
? CpuTempAggregation.MaxCore
|
||||
: CpuTempAggregation.AverageCore;
|
||||
|
||||
double? tempC = null;
|
||||
try
|
||||
{
|
||||
tempC = _cpu?.ReadCpuTemp(mode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugLog($"LHM: {ex.Message}");
|
||||
}
|
||||
|
||||
if (tempC == null)
|
||||
_consecutiveTempErrors++;
|
||||
else
|
||||
_consecutiveTempErrors = 0;
|
||||
|
||||
if (_consecutiveTempErrors >= cfg.FailSafeAfterConsecutiveErrors)
|
||||
_failSafeActive = true;
|
||||
else if (tempC != null)
|
||||
_failSafeActive = false;
|
||||
|
||||
byte? targetDuty = null;
|
||||
string? ectoolMsg = null;
|
||||
int? rpm = null;
|
||||
int? actualDuty = null;
|
||||
|
||||
if (_paused)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "paused";
|
||||
}
|
||||
else if (_failSafeActive)
|
||||
{
|
||||
ClearRampState();
|
||||
if (cfg.FailSafeRestoreAutoFan)
|
||||
{
|
||||
var (okA, _, eA) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs, ct)
|
||||
.ConfigureAwait(false);
|
||||
targetDuty = null;
|
||||
ectoolMsg = okA ? "fail-safe: autofanctrl" : $"fail-safe autofanctrl: {eA}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = (byte)Math.Clamp(cfg.FailSafeFanPercent, 0, 100);
|
||||
targetDuty = d;
|
||||
_lastCommandedDuty = d;
|
||||
var (okF, _, eF) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okF ? $"fail-safe fanduty {d}%" : $"fail-safe: {eF}";
|
||||
}
|
||||
}
|
||||
else if (tempC == null)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "no CPU temperature (holding EC state)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var curveTarget = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
||||
var output = ComputeRampOutput(curveTarget, cfg);
|
||||
targetDuty = output;
|
||||
_lastCommandedDuty = output;
|
||||
var (okD, _, eD) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", output.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okD ? $"fanduty {output}%" : eD;
|
||||
}
|
||||
|
||||
_cycleIndex++;
|
||||
if (cfg.FanRpmPollEveryNCycles > 0 &&
|
||||
_cycleIndex % cfg.FanRpmPollEveryNCycles == 0 &&
|
||||
File.Exists(cfg.EctoolPath))
|
||||
{
|
||||
var (okR, stdoutR, errR) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanRpmArgs, ct).ConfigureAwait(false);
|
||||
if (okR)
|
||||
rpm = EctoolRunner.TryParseFanRpm(stdoutR);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"rpm: {errR}" : $"{ectoolMsg}; rpm: {errR}";
|
||||
|
||||
if (cfg.FanDutyArgs is { Length: > 0 })
|
||||
{
|
||||
var (okDuty, stdoutDuty, errDuty) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanDutyArgs, ct).ConfigureAwait(false);
|
||||
if (okDuty)
|
||||
actualDuty = EctoolRunner.TryParseFanDuty(stdoutDuty);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"duty: {errDuty}" : $"{ectoolMsg}; duty: {errDuty}";
|
||||
}
|
||||
}
|
||||
|
||||
RaiseSample(new FanSampleEventArgs
|
||||
{
|
||||
Time = DateTime.Now,
|
||||
TempC = tempC,
|
||||
TargetDutyPercent = targetDuty,
|
||||
ActualDutyPercent = actualDuty,
|
||||
Rpm = rpm,
|
||||
LastEctoolMessage = ectoolMsg,
|
||||
Paused = _paused,
|
||||
FailSafe = _failSafeActive
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Math.Clamp(cfg.PollIntervalMs, 250, 60_000), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseSample(FanSampleEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Sample?.Invoke(this, e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* UI must not kill loop */
|
||||
}
|
||||
}
|
||||
|
||||
private static void DebugLog(string msg)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ChromeboxFanControl] {msg}");
|
||||
}
|
||||
|
||||
private void ClearRampState()
|
||||
{
|
||||
_rampStepIndex = 0;
|
||||
}
|
||||
|
||||
private byte ComputeRampOutput(byte targetDuty, AppConfig cfg)
|
||||
{
|
||||
var n = Math.Clamp(cfg.RampUpSteps, 1, 10);
|
||||
var minDelta = Math.Clamp(cfg.RampUpMinDeltaPercent, 0, 50);
|
||||
|
||||
if (_lastCommandedDuty == null)
|
||||
return targetDuty;
|
||||
|
||||
var curr = _lastCommandedDuty.Value;
|
||||
var delta = Math.Abs(targetDuty - curr);
|
||||
|
||||
if (targetDuty == curr)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (delta < minDelta)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (_rampStepIndex == 0 || targetDuty != _rampTargetDuty)
|
||||
{
|
||||
_rampStartDuty = curr;
|
||||
_rampTargetDuty = targetDuty;
|
||||
_rampStepIndex = 1;
|
||||
}
|
||||
|
||||
if (_rampStepIndex >= n)
|
||||
{
|
||||
ClearRampState();
|
||||
return _rampTargetDuty;
|
||||
}
|
||||
|
||||
var step = (int)Math.Round((_rampTargetDuty - _rampStartDuty) * (double)_rampStepIndex / n);
|
||||
var output = (byte)Math.Clamp(_rampStartDuty + step, 0, 100);
|
||||
_rampStepIndex++;
|
||||
return output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
}
|
||||
namespace ChromeboxFanControl;
|
||||
|
||||
/// <summary>
|
||||
/// Background loop: LHM temp → curve → ectool fanduty; ectool RPM; fail-safe handling.
|
||||
/// </summary>
|
||||
public sealed class FanController : IDisposable
|
||||
{
|
||||
private readonly object _cfgLock = new();
|
||||
private AppConfig _config;
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
private CpuTempReader? _cpu;
|
||||
private volatile bool _paused;
|
||||
private int _consecutiveTempErrors;
|
||||
private int _cycleIndex;
|
||||
private bool _failSafeActive;
|
||||
private byte? _lastCommandedDuty;
|
||||
private byte _rampStartDuty;
|
||||
private byte _rampTargetDuty;
|
||||
private int _rampStepIndex;
|
||||
|
||||
public FanController(AppConfig initialConfig)
|
||||
{
|
||||
_config = initialConfig;
|
||||
}
|
||||
|
||||
public event EventHandler<FanSampleEventArgs>? Sample;
|
||||
|
||||
public bool Paused
|
||||
{
|
||||
get => _paused;
|
||||
set => _paused = value;
|
||||
}
|
||||
|
||||
public void UpdateConfig(AppConfig config)
|
||||
{
|
||||
lock (_cfgLock)
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
_cpu = new CpuTempReader();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
_loop = Task.Run(() => LoopAsync(token), token);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
|
||||
private void StopInternal(bool waitForLoop)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
|
||||
if (waitForLoop && _loop != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_loop.Wait(8000);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* */
|
||||
}
|
||||
}
|
||||
|
||||
_cpu?.Dispose();
|
||||
_cpu = null;
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_loop = null;
|
||||
}
|
||||
|
||||
public async Task RestoreAutoFanAsync()
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
if (!File.Exists(cfg.EctoolPath))
|
||||
return;
|
||||
|
||||
var (ok, _, err) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs)
|
||||
.ConfigureAwait(false);
|
||||
if (!ok)
|
||||
DebugLog($"autofanctrl: {err}");
|
||||
}
|
||||
|
||||
private async Task LoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
AppConfig cfg;
|
||||
lock (_cfgLock)
|
||||
cfg = _config;
|
||||
|
||||
var mode = string.Equals(cfg.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase)
|
||||
? CpuTempAggregation.MaxCore
|
||||
: CpuTempAggregation.AverageCore;
|
||||
|
||||
double? tempC = null;
|
||||
try
|
||||
{
|
||||
tempC = _cpu?.ReadCpuTemp(mode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugLog($"LHM: {ex.Message}");
|
||||
}
|
||||
|
||||
if (tempC == null)
|
||||
_consecutiveTempErrors++;
|
||||
else
|
||||
_consecutiveTempErrors = 0;
|
||||
|
||||
if (_consecutiveTempErrors >= cfg.FailSafeAfterConsecutiveErrors)
|
||||
_failSafeActive = true;
|
||||
else if (tempC != null)
|
||||
_failSafeActive = false;
|
||||
|
||||
byte? targetDuty = null;
|
||||
string? ectoolMsg = null;
|
||||
int? rpm = null;
|
||||
int? actualDuty = null;
|
||||
|
||||
if (_paused)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "paused";
|
||||
}
|
||||
else if (_failSafeActive)
|
||||
{
|
||||
ClearRampState();
|
||||
if (cfg.FailSafeRestoreAutoFan)
|
||||
{
|
||||
var (okA, _, eA) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs, ct)
|
||||
.ConfigureAwait(false);
|
||||
targetDuty = null;
|
||||
ectoolMsg = okA ? "fail-safe: autofanctrl" : $"fail-safe autofanctrl: {eA}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = (byte)Math.Clamp(cfg.FailSafeFanPercent, 0, 100);
|
||||
targetDuty = d;
|
||||
_lastCommandedDuty = d;
|
||||
var (okF, _, eF) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okF ? $"fail-safe fanduty {d}%" : $"fail-safe: {eF}";
|
||||
}
|
||||
}
|
||||
else if (tempC == null)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "no CPU temperature (holding EC state)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var curveTarget = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
||||
var output = ComputeRampOutput(curveTarget, cfg);
|
||||
targetDuty = output;
|
||||
_lastCommandedDuty = output;
|
||||
var (okD, _, eD) = await EctoolRunner
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", output.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okD ? $"fanduty {output}%" : eD;
|
||||
}
|
||||
|
||||
_cycleIndex++;
|
||||
if (cfg.FanRpmPollEveryNCycles > 0 &&
|
||||
_cycleIndex % cfg.FanRpmPollEveryNCycles == 0 &&
|
||||
File.Exists(cfg.EctoolPath))
|
||||
{
|
||||
var (okR, stdoutR, errR) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanRpmArgs, ct).ConfigureAwait(false);
|
||||
if (okR)
|
||||
rpm = EctoolRunner.TryParseFanRpm(stdoutR);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"rpm: {errR}" : $"{ectoolMsg}; rpm: {errR}";
|
||||
|
||||
if (cfg.FanDutyArgs is { Length: > 0 })
|
||||
{
|
||||
var (okDuty, stdoutDuty, errDuty) =
|
||||
await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.FanDutyArgs, ct).ConfigureAwait(false);
|
||||
if (okDuty)
|
||||
actualDuty = EctoolRunner.TryParseFanDuty(stdoutDuty);
|
||||
else
|
||||
ectoolMsg = string.IsNullOrEmpty(ectoolMsg) ? $"duty: {errDuty}" : $"{ectoolMsg}; duty: {errDuty}";
|
||||
}
|
||||
}
|
||||
|
||||
RaiseSample(new FanSampleEventArgs
|
||||
{
|
||||
Time = DateTime.Now,
|
||||
TempC = tempC,
|
||||
TargetDutyPercent = targetDuty,
|
||||
ActualDutyPercent = actualDuty,
|
||||
Rpm = rpm,
|
||||
LastEctoolMessage = ectoolMsg,
|
||||
Paused = _paused,
|
||||
FailSafe = _failSafeActive
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Math.Clamp(cfg.PollIntervalMs, 250, 60_000), ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseSample(FanSampleEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Sample?.Invoke(this, e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* UI must not kill loop */
|
||||
}
|
||||
}
|
||||
|
||||
private static void DebugLog(string msg)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[ChromeboxFanControl] {msg}");
|
||||
}
|
||||
|
||||
private void ClearRampState()
|
||||
{
|
||||
_rampStepIndex = 0;
|
||||
}
|
||||
|
||||
private byte ComputeRampOutput(byte targetDuty, AppConfig cfg)
|
||||
{
|
||||
var n = Math.Clamp(cfg.RampUpSteps, 1, 10);
|
||||
var minDelta = Math.Clamp(cfg.RampUpMinDeltaPercent, 0, 50);
|
||||
|
||||
if (_lastCommandedDuty == null)
|
||||
return targetDuty;
|
||||
|
||||
var curr = _lastCommandedDuty.Value;
|
||||
var delta = Math.Abs(targetDuty - curr);
|
||||
|
||||
if (targetDuty == curr)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (delta < minDelta)
|
||||
{
|
||||
ClearRampState();
|
||||
return targetDuty;
|
||||
}
|
||||
|
||||
if (_rampStepIndex == 0 || targetDuty != _rampTargetDuty)
|
||||
{
|
||||
_rampStartDuty = curr;
|
||||
_rampTargetDuty = targetDuty;
|
||||
_rampStepIndex = 1;
|
||||
}
|
||||
|
||||
if (_rampStepIndex >= n)
|
||||
{
|
||||
ClearRampState();
|
||||
return _rampTargetDuty;
|
||||
}
|
||||
|
||||
var step = (int)Math.Round((_rampTargetDuty - _rampStartDuty) * (double)_rampStepIndex / n);
|
||||
var output = (byte)Math.Clamp(_rampStartDuty + step, 0, 100);
|
||||
_rampStepIndex++;
|
||||
return output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopInternal(waitForLoop: true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user