Files
chromebox-fan-control-win/ChromeboxFanControl/FanController.cs
2026-03-21 05:47:58 +08:00

241 lines
6.9 KiB
C#

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;
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)
{
// Do not send fanduty while paused; EC keeps last hardware state.
targetDuty = null;
ectoolMsg = "paused";
}
else if (_failSafeActive)
{
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;
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)
{
targetDuty = null;
ectoolMsg = "no CPU temperature (holding EC state)";
}
else
{
var d = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
targetDuty = d;
var (okD, _, eD) = await EctoolRunner
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
.ConfigureAwait(false);
ectoolMsg = okD ? $"fanduty {d}%" : 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}");
}
public void Dispose()
{
StopInternal(waitForLoop: true);
}
}