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[]? FanDutyArgs { get; set; }
|
||||||
public string[] AutoFanCtrlArgs { get; set; } = ["autofanctrl"];
|
public string[] AutoFanCtrlArgs { get; set; } = ["autofanctrl"];
|
||||||
public string TempSource { get; set; } = "AverageCore";
|
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 FailSafeAfterConsecutiveErrors { get; set; } = 5;
|
||||||
public int FailSafeFanPercent { get; set; } = 100;
|
public int FailSafeFanPercent { get; set; } = 100;
|
||||||
public bool FailSafeRestoreAutoFan { get; set; }
|
public bool FailSafeRestoreAutoFan { get; set; }
|
||||||
@@ -81,6 +83,8 @@ public sealed class AppConfig
|
|||||||
if (src.AutoFanCtrlArgs is { Length: > 0 })
|
if (src.AutoFanCtrlArgs is { Length: > 0 })
|
||||||
dst.AutoFanCtrlArgs = src.AutoFanCtrlArgs;
|
dst.AutoFanCtrlArgs = src.AutoFanCtrlArgs;
|
||||||
dst.TempSource = string.Equals(src.TempSource, "MaxCore", StringComparison.OrdinalIgnoreCase) ? "MaxCore" : "AverageCore";
|
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.FailSafeAfterConsecutiveErrors = Math.Max(1, src.FailSafeAfterConsecutiveErrors);
|
||||||
dst.FailSafeFanPercent = Math.Clamp(src.FailSafeFanPercent, 0, 100);
|
dst.FailSafeFanPercent = Math.Clamp(src.FailSafeFanPercent, 0, 100);
|
||||||
dst.FailSafeRestoreAutoFan = src.FailSafeRestoreAutoFan;
|
dst.FailSafeRestoreAutoFan = src.FailSafeRestoreAutoFan;
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ public sealed class FanController : IDisposable
|
|||||||
private int _consecutiveTempErrors;
|
private int _consecutiveTempErrors;
|
||||||
private int _cycleIndex;
|
private int _cycleIndex;
|
||||||
private bool _failSafeActive;
|
private bool _failSafeActive;
|
||||||
|
private byte? _lastCommandedDuty;
|
||||||
|
private byte _rampStartDuty;
|
||||||
|
private byte _rampTargetDuty;
|
||||||
|
private int _rampStepIndex;
|
||||||
|
|
||||||
public FanController(AppConfig initialConfig)
|
public FanController(AppConfig initialConfig)
|
||||||
{
|
{
|
||||||
@@ -132,12 +136,13 @@ public sealed class FanController : IDisposable
|
|||||||
|
|
||||||
if (_paused)
|
if (_paused)
|
||||||
{
|
{
|
||||||
// Do not send fanduty while paused; EC keeps last hardware state.
|
ClearRampState();
|
||||||
targetDuty = null;
|
targetDuty = null;
|
||||||
ectoolMsg = "paused";
|
ectoolMsg = "paused";
|
||||||
}
|
}
|
||||||
else if (_failSafeActive)
|
else if (_failSafeActive)
|
||||||
{
|
{
|
||||||
|
ClearRampState();
|
||||||
if (cfg.FailSafeRestoreAutoFan)
|
if (cfg.FailSafeRestoreAutoFan)
|
||||||
{
|
{
|
||||||
var (okA, _, eA) = await EctoolRunner.RunAsync(cfg.EctoolPath, cfg.AutoFanCtrlArgs, ct)
|
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);
|
var d = (byte)Math.Clamp(cfg.FailSafeFanPercent, 0, 100);
|
||||||
targetDuty = d;
|
targetDuty = d;
|
||||||
|
_lastCommandedDuty = d;
|
||||||
var (okF, _, eF) = await EctoolRunner
|
var (okF, _, eF) = await EctoolRunner
|
||||||
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -157,17 +163,20 @@ public sealed class FanController : IDisposable
|
|||||||
}
|
}
|
||||||
else if (tempC == null)
|
else if (tempC == null)
|
||||||
{
|
{
|
||||||
|
ClearRampState();
|
||||||
targetDuty = null;
|
targetDuty = null;
|
||||||
ectoolMsg = "no CPU temperature (holding EC state)";
|
ectoolMsg = "no CPU temperature (holding EC state)";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var d = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
var curveTarget = FanCurve.CalculateFanPercent(tempC.Value, cfg.CurvePoints);
|
||||||
targetDuty = d;
|
var output = ComputeRampOutput(curveTarget, cfg);
|
||||||
|
targetDuty = output;
|
||||||
|
_lastCommandedDuty = output;
|
||||||
var (okD, _, eD) = await EctoolRunner
|
var (okD, _, eD) = await EctoolRunner
|
||||||
.RunAsync(cfg.EctoolPath, ["fanduty", d.ToString()], ct)
|
.RunAsync(cfg.EctoolPath, ["fanduty", output.ToString()], ct)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
ectoolMsg = okD ? $"fanduty {d}%" : eD;
|
ectoolMsg = okD ? $"fanduty {output}%" : eD;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cycleIndex++;
|
_cycleIndex++;
|
||||||
@@ -233,6 +242,53 @@ public sealed class FanController : IDisposable
|
|||||||
System.Diagnostics.Debug.WriteLine($"[ChromeboxFanControl] {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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
StopInternal(waitForLoop: true);
|
StopInternal(waitForLoop: true);
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public sealed class MainForm : Form
|
|||||||
private readonly CheckBox _chkFailAuto = new();
|
private readonly CheckBox _chkFailAuto = new();
|
||||||
private readonly NumericUpDown _nudChartMin = new();
|
private readonly NumericUpDown _nudChartMin = new();
|
||||||
private readonly NumericUpDown _nudChartPoints = new();
|
private readonly NumericUpDown _nudChartPoints = new();
|
||||||
|
private readonly NumericUpDown _nudRampUpSteps = new();
|
||||||
|
private readonly NumericUpDown _nudRampUpMinDelta = new();
|
||||||
private readonly NotifyIcon _tray = new();
|
private readonly NotifyIcon _tray = new();
|
||||||
private readonly ContextMenuStrip _trayMenu = new();
|
private readonly ContextMenuStrip _trayMenu = new();
|
||||||
private readonly ToolStripMenuItem _miPause;
|
private readonly ToolStripMenuItem _miPause;
|
||||||
@@ -141,11 +143,13 @@ public sealed class MainForm : Form
|
|||||||
{
|
{
|
||||||
Dock = DockStyle.Fill,
|
Dock = DockStyle.Fill,
|
||||||
ColumnCount = 2,
|
ColumnCount = 2,
|
||||||
RowCount = 13,
|
RowCount = 15,
|
||||||
Padding = new Padding(12)
|
Padding = new Padding(12)
|
||||||
};
|
};
|
||||||
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35));
|
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35));
|
||||||
flp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 65));
|
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;
|
var row = 0;
|
||||||
AddRow(flp, row++, Resources.LblLanguage, _cmbLanguage);
|
AddRow(flp, row++, Resources.LblLanguage, _cmbLanguage);
|
||||||
@@ -201,6 +205,14 @@ public sealed class MainForm : Form
|
|||||||
_nudChartPoints.Maximum = 10_000;
|
_nudChartPoints.Maximum = 10_000;
|
||||||
_nudChartPoints.Increment = 100;
|
_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 };
|
var btnSaveAdv = new Button { Text = Resources.BtnSaveAdvanced, AutoSize = true, Dock = DockStyle.Bottom };
|
||||||
btnSaveAdv.Click += (_, _) => SaveFromUi();
|
btnSaveAdv.Click += (_, _) => SaveFromUi();
|
||||||
tabAdv.Controls.Add(btnSaveAdv);
|
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)
|
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;
|
editor.Dock = DockStyle.Fill;
|
||||||
t.Controls.Add(editor, 1, row);
|
t.Controls.Add(editor, 1, row);
|
||||||
}
|
}
|
||||||
@@ -271,6 +283,8 @@ public sealed class MainForm : Form
|
|||||||
_chkFailAuto.Checked = _config.FailSafeRestoreAutoFan;
|
_chkFailAuto.Checked = _config.FailSafeRestoreAutoFan;
|
||||||
_nudChartMin.Value = _config.ChartHistoryMinutes;
|
_nudChartMin.Value = _config.ChartHistoryMinutes;
|
||||||
_nudChartPoints.Value = _config.ChartMaxPoints;
|
_nudChartPoints.Value = _config.ChartMaxPoints;
|
||||||
|
_nudRampUpSteps.Value = _config.RampUpSteps;
|
||||||
|
_nudRampUpMinDelta.Value = _config.RampUpMinDeltaPercent;
|
||||||
for (var i = 0; i < 14; i++)
|
for (var i = 0; i < 14; i++)
|
||||||
_gridCurve.Rows[i].Cells[1].Value = _config.CurvePoints[i].ToString();
|
_gridCurve.Rows[i].Cells[1].Value = _config.CurvePoints[i].ToString();
|
||||||
RebuildCurveChart();
|
RebuildCurveChart();
|
||||||
@@ -296,6 +310,8 @@ public sealed class MainForm : Form
|
|||||||
FailSafeRestoreAutoFan = _chkFailAuto.Checked,
|
FailSafeRestoreAutoFan = _chkFailAuto.Checked,
|
||||||
ChartHistoryMinutes = (int)_nudChartMin.Value,
|
ChartHistoryMinutes = (int)_nudChartMin.Value,
|
||||||
ChartMaxPoints = (int)_nudChartPoints.Value,
|
ChartMaxPoints = (int)_nudChartPoints.Value,
|
||||||
|
RampUpSteps = (int)_nudRampUpSteps.Value,
|
||||||
|
RampUpMinDeltaPercent = (int)_nudRampUpMinDelta.Value,
|
||||||
CurvePoints = ReadCurveFromGrid()
|
CurvePoints = ReadCurveFromGrid()
|
||||||
};
|
};
|
||||||
return c;
|
return c;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ internal static class Resources
|
|||||||
public static string LblFailStrategy => _rm.GetString("LblFailStrategy") ?? "Fail-safe strategy";
|
public static string LblFailStrategy => _rm.GetString("LblFailStrategy") ?? "Fail-safe strategy";
|
||||||
public static string LblChartMin => _rm.GetString("LblChartMin") ?? "Chart minutes";
|
public static string LblChartMin => _rm.GetString("LblChartMin") ?? "Chart minutes";
|
||||||
public static string LblChartPoints => _rm.GetString("LblChartPoints") ?? "Chart points";
|
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 BtnSaveAdvanced => _rm.GetString("BtnSaveAdvanced") ?? "Save Advanced";
|
||||||
public static string TrayOpen => _rm.GetString("TrayOpen") ?? "Show window";
|
public static string TrayOpen => _rm.GetString("TrayOpen") ?? "Show window";
|
||||||
public static string TrayPause => _rm.GetString("TrayPause") ?? "Pause";
|
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="LblFailStrategy" xml:space="preserve"><value>Fail-safe strategy</value></data>
|
||||||
<data name="LblChartMin" xml:space="preserve"><value>Chart history (minutes)</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="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="BtnSaveAdvanced" xml:space="preserve"><value>Save Advanced Settings</value></data>
|
||||||
<data name="TrayOpen" xml:space="preserve"><value>Show window</value></data>
|
<data name="TrayOpen" xml:space="preserve"><value>Show window</value></data>
|
||||||
<data name="TrayPause" xml:space="preserve"><value>Pause control</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="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||||
<data name="LblChartMin" 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="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="BtnSaveAdvanced" xml:space="preserve"><value>保存高级设置</value></data>
|
||||||
<data name="TrayOpen" xml:space="preserve"><value>打开主窗口</value></data>
|
<data name="TrayOpen" xml:space="preserve"><value>打开主窗口</value></data>
|
||||||
<data name="TrayPause" 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="LblFailStrategy" xml:space="preserve"><value>安全模式策略</value></data>
|
||||||
<data name="LblChartMin" 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="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="BtnSaveAdvanced" xml:space="preserve"><value>儲存進階設定</value></data>
|
||||||
<data name="TrayOpen" xml:space="preserve"><value>開啟主視窗</value></data>
|
<data name="TrayOpen" xml:space="preserve"><value>開啟主視窗</value></data>
|
||||||
<data name="TrayPause" xml:space="preserve"><value>暫停控制</value></data>
|
<data name="TrayPause" xml:space="preserve"><value>暫停控制</value></data>
|
||||||
|
|||||||
Reference in New Issue
Block a user