diff --git a/ChromeboxFanControl/AppConfig.cs b/ChromeboxFanControl/AppConfig.cs index 25221dc..6acaa90 100644 --- a/ChromeboxFanControl/AppConfig.cs +++ b/ChromeboxFanControl/AppConfig.cs @@ -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; diff --git a/ChromeboxFanControl/FanController.cs b/ChromeboxFanControl/FanController.cs index 6f87536..1c0be6e 100644 --- a/ChromeboxFanControl/FanController.cs +++ b/ChromeboxFanControl/FanController.cs @@ -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); diff --git a/ChromeboxFanControl/MainForm.cs b/ChromeboxFanControl/MainForm.cs index a70b4b0..3f6caa8 100644 --- a/ChromeboxFanControl/MainForm.cs +++ b/ChromeboxFanControl/MainForm.cs @@ -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; diff --git a/ChromeboxFanControl/Properties/Resources.Designer.cs b/ChromeboxFanControl/Properties/Resources.Designer.cs index f3901a7..3cadd83 100644 --- a/ChromeboxFanControl/Properties/Resources.Designer.cs +++ b/ChromeboxFanControl/Properties/Resources.Designer.cs @@ -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"; diff --git a/ChromeboxFanControl/Properties/Resources.resx b/ChromeboxFanControl/Properties/Resources.resx index 45d11df..f801ab0 100644 --- a/ChromeboxFanControl/Properties/Resources.resx +++ b/ChromeboxFanControl/Properties/Resources.resx @@ -83,6 +83,8 @@ Fail-safe strategy Chart history (minutes) Chart max points + Ramp-up steps + Min ramp delta (%) Save Advanced Settings Show window Pause control diff --git a/ChromeboxFanControl/Properties/Resources.zh-Hans.resx b/ChromeboxFanControl/Properties/Resources.zh-Hans.resx index e4e7edf..7f9692c 100644 --- a/ChromeboxFanControl/Properties/Resources.zh-Hans.resx +++ b/ChromeboxFanControl/Properties/Resources.zh-Hans.resx @@ -83,6 +83,8 @@ 安全模式策略 图表保留时长 (分钟) 图表最大点数 + 升速步数 + 最小 ramp 增幅 (%) 保存高级设置 打开主窗口 暂停控制 diff --git a/ChromeboxFanControl/Properties/Resources.zh-Hant.resx b/ChromeboxFanControl/Properties/Resources.zh-Hant.resx index d62e940..44777f8 100644 --- a/ChromeboxFanControl/Properties/Resources.zh-Hant.resx +++ b/ChromeboxFanControl/Properties/Resources.zh-Hant.resx @@ -83,6 +83,8 @@ 安全模式策略 圖表保留時長 (分鐘) 圖表最大點數 + 升速步數 + 最小 ramp 增幅 (%) 儲存進階設定 開啟主視窗 暫停控制