feat: ramp-up/down delay fan control
- Add RampUpSteps and RampUpMinDeltaPercent config - Gradual ramp in both directions (up and down) to reduce fan oscillation - Fix Advanced tab layout with RowStyles and label alignment Made-with: Cursor
This commit is contained in:
@@ -16,6 +16,8 @@ public sealed class AppConfig
|
||||
public string[]? FanDutyArgs { get; set; }
|
||||
public string[] AutoFanCtrlArgs { get; set; } = ["autofanctrl"];
|
||||
public string TempSource { get; set; } = "AverageCore";
|
||||
public int RampUpSteps { get; set; } = 3;
|
||||
public int RampUpMinDeltaPercent { get; set; } = 20;
|
||||
public int FailSafeAfterConsecutiveErrors { get; set; } = 5;
|
||||
public int FailSafeFanPercent { get; set; } = 100;
|
||||
public bool FailSafeRestoreAutoFan { get; set; }
|
||||
@@ -81,6 +83,8 @@ public sealed class AppConfig
|
||||
if (src.AutoFanCtrlArgs is { Length: > 0 })
|
||||
dst.AutoFanCtrlArgs = src.AutoFanCtrlArgs;
|
||||
dst.TempSource = string.Equals(src.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase) ? "MaxCore" : "AverageCore";
|
||||
dst.RampUpSteps = Math.Clamp(src.RampUpSteps, 1, 10);
|
||||
dst.RampUpMinDeltaPercent = Math.Clamp(src.RampUpMinDeltaPercent, 0, 50);
|
||||
dst.FailSafeAfterConsecutiveErrors = Math.Max(1, src.FailSafeAfterConsecutiveErrors);
|
||||
dst.FailSafeFanPercent = Math.Clamp(src.FailSafeFanPercent, 0, 100);
|
||||
dst.FailSafeRestoreAutoFan = src.FailSafeRestoreAutoFan;
|
||||
|
||||
@@ -14,6 +14,10 @@ public sealed class FanController : IDisposable
|
||||
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)
|
||||
{
|
||||
@@ -132,12 +136,13 @@ public sealed class FanController : IDisposable
|
||||
|
||||
if (_paused)
|
||||
{
|
||||
// Do not send fanduty while paused; EC keeps last hardware state.
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "paused";
|
||||
}
|
||||
else if (_failSafeActive)
|
||||
{
|
||||
ClearRampState();
|
||||
if (cfg.FailSafeRestoreAutoFan)
|
||||
{
|
||||
var (okA, _, eA) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs, ct)
|
||||
@@ -149,6 +154,7 @@ public sealed class FanController : IDisposable
|
||||
{
|
||||
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);
|
||||
@@ -157,17 +163,20 @@ public sealed class FanController : IDisposable
|
||||
}
|
||||
else if (tempC == null)
|
||||
{
|
||||
ClearRampState();
|
||||
targetDuty = null;
|
||||
ectoolMsg = "no CPU temperature (holding EC state)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var d = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
||||
targetDuty = d;
|
||||
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", d.ToString()], ct)
|
||||
.RunAsync(cfg.EctoolPath, ["fanduty", output.ToString()], ct)
|
||||
.ConfigureAwait(false);
|
||||
ectoolMsg = okD ? $"fanduty {d}%" : eD;
|
||||
ectoolMsg = okD ? $"fanduty {output}%" : eD;
|
||||
}
|
||||
|
||||
_cycleIndex++;
|
||||
@@ -233,6 +242,53 @@ public sealed class FanController : IDisposable
|
||||
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);
|
||||
|
||||
@@ -25,6 +25,8 @@ public sealed class MainForm : Form
|
||||
private readonly CheckBox _chkFailAuto = new();
|
||||
private readonly NumericUpDown _nudChartMin = new();
|
||||
private readonly NumericUpDown _nudChartPoints = new();
|
||||
private readonly NumericUpDown _nudRampUpSteps = new();
|
||||
private readonly NumericUpDown _nudRampUpMinDelta = new();
|
||||
private readonly NotifyIcon _tray = new();
|
||||
private readonly ContextMenuStrip _trayMenu = new();
|
||||
private readonly ToolStripMenuItem _miPause;
|
||||
@@ -141,11 +143,13 @@ public sealed class MainForm : Form
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 2,
|
||||
RowCount = 13,
|
||||
RowCount = 15,
|
||||
Padding = new Padding(12)
|
||||
};
|
||||
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35));
|
||||
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 65));
|
||||
for (var i = 0; i < 15; i++)
|
||||
flp.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
var row = 0;
|
||||
AddRow(flp, row++, Resources.LblLanguage, _cmbLanguage);
|
||||
@@ -201,6 +205,14 @@ public sealed class MainForm : Form
|
||||
_nudChartPoints.Maximum = 10_000;
|
||||
_nudChartPoints.Increment = 100;
|
||||
|
||||
AddRow(flp, row++, Resources.LblRampUpSteps, _nudRampUpSteps);
|
||||
_nudRampUpSteps.Minimum = 1;
|
||||
_nudRampUpSteps.Maximum = 10;
|
||||
|
||||
AddRow(flp, row++, Resources.LblRampUpMinDeltaPercent, _nudRampUpMinDelta);
|
||||
_nudRampUpMinDelta.Minimum = 0;
|
||||
_nudRampUpMinDelta.Maximum = 50;
|
||||
|
||||
var btnSaveAdv = new Button { Text = Resources.BtnSaveAdvanced, AutoSize = true, Dock = DockStyle.Bottom };
|
||||
btnSaveAdv.Click += (_, _) => SaveFromUi();
|
||||
tabAdv.Controls.Add(btnSaveAdv);
|
||||
@@ -211,7 +223,7 @@ public sealed class MainForm : Form
|
||||
|
||||
private static void AddRow(TableLayoutPanel t, int row, string label, Control editor)
|
||||
{
|
||||
t.Controls.Add(new System.Windows.Forms.Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left }, 0, row);
|
||||
t.Controls.Add(new System.Windows.Forms.Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left | AnchorStyles.Top }, 0, row);
|
||||
editor.Dock = DockStyle.Fill;
|
||||
t.Controls.Add(editor, 1, row);
|
||||
}
|
||||
@@ -271,6 +283,8 @@ public sealed class MainForm : Form
|
||||
_chkFailAuto.Checked = _config.FailSafeRestoreAutoFan;
|
||||
_nudChartMin.Value = _config.ChartHistoryMinutes;
|
||||
_nudChartPoints.Value = _config.ChartMaxPoints;
|
||||
_nudRampUpSteps.Value = _config.RampUpSteps;
|
||||
_nudRampUpMinDelta.Value = _config.RampUpMinDeltaPercent;
|
||||
for (var i = 0; i < 14; i++)
|
||||
_gridCurve.Rows[i].Cells[1].Value = _config.CurvePoints[i].ToString();
|
||||
RebuildCurveChart();
|
||||
@@ -296,6 +310,8 @@ public sealed class MainForm : Form
|
||||
FailSafeRestoreAutoFan = _chkFailAuto.Checked,
|
||||
ChartHistoryMinutes = (int)_nudChartMin.Value,
|
||||
ChartMaxPoints = (int)_nudChartPoints.Value,
|
||||
RampUpSteps = (int)_nudRampUpSteps.Value,
|
||||
RampUpMinDeltaPercent = (int)_nudRampUpMinDelta.Value,
|
||||
CurvePoints = ReadCurveFromGrid()
|
||||
};
|
||||
return c;
|
||||
|
||||
@@ -33,6 +33,8 @@ internal static class Resources
|
||||
public static string LblFailStrategy => _rm.GetString("LblFailStrategy") ?? "Fail-safe strategy";
|
||||
public static string LblChartMin => _rm.GetString("LblChartMin") ?? "Chart minutes";
|
||||
public static string LblChartPoints => _rm.GetString("LblChartPoints") ?? "Chart points";
|
||||
public static string LblRampUpSteps => _rm.GetString("LblRampUpSteps") ?? "Ramp-up steps";
|
||||
public static string LblRampUpMinDeltaPercent => _rm.GetString("LblRampUpMinDeltaPercent") ?? "Min ramp delta (%)";
|
||||
public static string BtnSaveAdvanced => _rm.GetString("BtnSaveAdvanced") ?? "Save Advanced";
|
||||
public static string TrayOpen => _rm.GetString("TrayOpen") ?? "Show window";
|
||||
public static string TrayPause => _rm.GetString("TrayPause") ?? "Pause";
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>Fail-safe strategy</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>Chart history (minutes)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>Chart max points</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>Ramp-up steps</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>Min ramp delta (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>Save Advanced Settings</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>Show window</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>Pause control</value></data>
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>图表保留时长 (分钟)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>图表最大点数</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>升速步数</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>最小 ramp 增幅 (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>保存高级设置</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>打开主窗口</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>暂停控制</value></data>
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
<data name="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||
<data name="LblChartMin" xml:space="preserve"><value>圖表保留時長 (分鐘)</value></data>
|
||||
<data name="LblChartPoints" xml:space="preserve"><value>圖表最大點數</value></data>
|
||||
<data name="LblRampUpSteps" xml:space="preserve"><value>升速步數</value></data>
|
||||
<data name="LblRampUpMinDeltaPercent" xml:space="preserve"><value>最小 ramp 增幅 (%)</value></data>
|
||||
<data name="BtnSaveAdvanced" xml:space="preserve"><value>儲存進階設定</value></data>
|
||||
<data name="TrayOpen" xml:space="preserve"><value>開啟主視窗</value></data>
|
||||
<data name="TrayPause" xml:space="preserve"><value>暫停控制</value></data>
|
||||
|
||||
Reference in New Issue
Block a user