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 增幅 (%)
儲存進階設定
開啟主視窗
暫停控制