From c94d5883fef63ed61b158d79a2b7ce2cbf094221 Mon Sep 17 00:00:00 2001 From: Eric Evans Date: Sun, 12 Apr 2026 19:51:39 -0700 Subject: [PATCH] Add Windows startup and draggable fan curve control --- AsusFanControl/AsusControl.cs | 12 +- AsusFanControl/AsusFanControl.csproj | 5 + AsusFanControl/Properties/AssemblyInfo.cs | 4 +- AsusFanControlGUI/App.config | 9 + AsusFanControlGUI/AsusFanControlGUI.csproj | 12 + AsusFanControlGUI/FanCurve.cs | 167 ++++++ AsusFanControlGUI/FanCurveGraphControl.cs | 321 ++++++++++ AsusFanControlGUI/Form1.cs | 551 +++++++++++++++--- AsusFanControlGUI/Program.cs | 7 +- AsusFanControlGUI/Properties/AssemblyInfo.cs | 4 +- .../Properties/Settings.Designer.cs | 36 ++ .../Properties/Settings.settings | 9 + AsusFanControlGUI/StartupRegistration.cs | 319 ++++++++++ CHANGELOG.md | 30 + README.md | 17 +- run.bat | 19 + 16 files changed, 1436 insertions(+), 86 deletions(-) create mode 100644 AsusFanControlGUI/FanCurve.cs create mode 100644 AsusFanControlGUI/FanCurveGraphControl.cs create mode 100644 AsusFanControlGUI/StartupRegistration.cs create mode 100644 CHANGELOG.md create mode 100644 run.bat diff --git a/AsusFanControl/AsusControl.cs b/AsusFanControl/AsusControl.cs index 69ccd9a..f9dba87 100644 --- a/AsusFanControl/AsusControl.cs +++ b/AsusFanControl/AsusControl.cs @@ -1,9 +1,7 @@ using AsusSystemAnalysis; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Threading; namespace AsusFanControl { @@ -32,13 +30,15 @@ public void SetFanSpeed(int percent, byte fanIndex = 0) SetFanSpeed(value, fanIndex); } - public async void SetFanSpeeds(byte value) + public void SetFanSpeeds(byte value) { var fanCount = AsusWinIO64.HealthyTable_FanCounts(); - for(byte fanIndex = 0; fanIndex < fanCount; fanIndex++) + for (byte fanIndex = 0; fanIndex < fanCount; fanIndex++) { SetFanSpeed(value, fanIndex); - await Task.Delay(20); + + if (fanIndex + 1 < fanCount) + Thread.Sleep(20); } } diff --git a/AsusFanControl/AsusFanControl.csproj b/AsusFanControl/AsusFanControl.csproj index 4f06f7f..d9896e9 100644 --- a/AsusFanControl/AsusFanControl.csproj +++ b/AsusFanControl/AsusFanControl.csproj @@ -76,5 +76,10 @@ PreserveNewest + + + All + + \ No newline at end of file diff --git a/AsusFanControl/Properties/AssemblyInfo.cs b/AsusFanControl/Properties/AssemblyInfo.cs index baefc56..9b583e1 100644 --- a/AsusFanControl/Properties/AssemblyInfo.cs +++ b/AsusFanControl/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.2.0.0")] +[assembly: AssemblyFileVersion("1.2.0.0")] diff --git a/AsusFanControlGUI/App.config b/AsusFanControlGUI/App.config index d169e06..4de6eaa 100644 --- a/AsusFanControlGUI/App.config +++ b/AsusFanControlGUI/App.config @@ -13,6 +13,9 @@ 90 + + False + True @@ -25,6 +28,12 @@ False + + False + + + 45:40;60:55;70:70;80:85;90:99 + \ No newline at end of file diff --git a/AsusFanControlGUI/AsusFanControlGUI.csproj b/AsusFanControlGUI/AsusFanControlGUI.csproj index d1527a6..0233d6e 100644 --- a/AsusFanControlGUI/AsusFanControlGUI.csproj +++ b/AsusFanControlGUI/AsusFanControlGUI.csproj @@ -69,6 +69,8 @@ + + Form @@ -77,6 +79,7 @@ + Form1.cs Designer @@ -102,6 +105,10 @@ + + run.bat + PreserveNewest + @@ -112,5 +119,10 @@ + + + All + + \ No newline at end of file diff --git a/AsusFanControlGUI/FanCurve.cs b/AsusFanControlGUI/FanCurve.cs new file mode 100644 index 0000000..05c6961 --- /dev/null +++ b/AsusFanControlGUI/FanCurve.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace AsusFanControlGUI +{ + internal sealed class FanCurve + { + public static FanCurve Default => new FanCurve(new[] + { + new FanCurvePoint(45, 40), + new FanCurvePoint(60, 55), + new FanCurvePoint(70, 70), + new FanCurvePoint(80, 85), + new FanCurvePoint(90, 99), + }); + + public FanCurve(IEnumerable points) + { + if (points == null) + throw new ArgumentNullException(nameof(points)); + + var orderedPoints = points.OrderBy(point => point.Temperature).ToList(); + if (orderedPoints.Count < 2) + throw new ArgumentException("A fan curve needs at least two points.", nameof(points)); + + for (var index = 0; index < orderedPoints.Count; index++) + { + var point = orderedPoints[index]; + if (point.Temperature < 0 || point.Temperature > 120) + throw new ArgumentOutOfRangeException(nameof(points), "Curve temperatures must be between 0 and 120 C."); + + if (point.FanSpeed < 0 || point.FanSpeed > 100) + throw new ArgumentOutOfRangeException(nameof(points), "Curve fan speeds must be between 0 and 100 percent."); + + if (index > 0 && point.Temperature <= orderedPoints[index - 1].Temperature) + throw new ArgumentException("Curve temperatures must be strictly increasing.", nameof(points)); + } + + Points = orderedPoints; + } + + public IReadOnlyList Points { get; } + + public int GetFanSpeed(int temperature) + { + if (temperature <= Points[0].Temperature) + return Points[0].FanSpeed; + + for (var index = 1; index < Points.Count; index++) + { + var lowerPoint = Points[index - 1]; + var upperPoint = Points[index]; + if (temperature <= upperPoint.Temperature) + { + var temperatureDelta = upperPoint.Temperature - lowerPoint.Temperature; + if (temperatureDelta <= 0) + return upperPoint.FanSpeed; + + var progress = (temperature - lowerPoint.Temperature) / (double)temperatureDelta; + var interpolatedSpeed = lowerPoint.FanSpeed + ((upperPoint.FanSpeed - lowerPoint.FanSpeed) * progress); + return (int)Math.Round(interpolatedSpeed, MidpointRounding.AwayFromZero); + } + } + + return Points[Points.Count - 1].FanSpeed; + } + + public int GetFanSpeed(int temperature, int minimumTemperature, int maximumTemperature, int minimumFanSpeed, int maximumFanSpeed) + { + var normalizedMinimumTemperature = Math.Max(0, minimumTemperature); + var normalizedMaximumTemperature = Math.Max(normalizedMinimumTemperature, maximumTemperature); + var normalizedMinimumFanSpeed = Math.Max(0, Math.Min(100, minimumFanSpeed)); + var normalizedMaximumFanSpeed = Math.Max(normalizedMinimumFanSpeed, Math.Min(100, maximumFanSpeed)); + + var curvePoints = new List + { + new FanCurvePoint(normalizedMinimumTemperature, normalizedMinimumFanSpeed) + }; + curvePoints.AddRange(Points); + curvePoints.Add(new FanCurvePoint(normalizedMaximumTemperature, normalizedMaximumFanSpeed)); + + if (temperature <= curvePoints[0].Temperature) + return curvePoints[0].FanSpeed; + + for (var index = 1; index < curvePoints.Count; index++) + { + var lowerPoint = curvePoints[index - 1]; + var upperPoint = curvePoints[index]; + if (temperature <= upperPoint.Temperature) + { + var temperatureDelta = upperPoint.Temperature - lowerPoint.Temperature; + if (temperatureDelta <= 0) + return upperPoint.FanSpeed; + + var progress = (temperature - lowerPoint.Temperature) / (double)temperatureDelta; + var interpolatedSpeed = lowerPoint.FanSpeed + ((upperPoint.FanSpeed - lowerPoint.FanSpeed) * progress); + return (int)Math.Round(interpolatedSpeed, MidpointRounding.AwayFromZero); + } + } + + return curvePoints[curvePoints.Count - 1].FanSpeed; + } + + public string Serialize() + { + return string.Join(";", Points.Select(point => + string.Format(CultureInfo.InvariantCulture, "{0}:{1}", point.Temperature, point.FanSpeed))); + } + + public static FanCurve ParseOrDefault(string serializedCurve) + { + if (TryParse(serializedCurve, out var fanCurve)) + return fanCurve; + + return Default; + } + + public static bool TryParse(string serializedCurve, out FanCurve fanCurve) + { + fanCurve = null; + + if (string.IsNullOrWhiteSpace(serializedCurve)) + return false; + + var points = new List(); + foreach (var rawPoint in serializedCurve.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + var segments = rawPoint.Split(':'); + if (segments.Length != 2) + return false; + + if (!int.TryParse(segments[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var temperature)) + return false; + + if (!int.TryParse(segments[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var fanSpeed)) + return false; + + points.Add(new FanCurvePoint(temperature, fanSpeed)); + } + + try + { + fanCurve = new FanCurve(points); + return true; + } + catch (ArgumentException) + { + return false; + } + } + } + + internal sealed class FanCurvePoint + { + public FanCurvePoint(int temperature, int fanSpeed) + { + Temperature = temperature; + FanSpeed = fanSpeed; + } + + public int Temperature { get; } + + public int FanSpeed { get; } + } +} \ No newline at end of file diff --git a/AsusFanControlGUI/FanCurveGraphControl.cs b/AsusFanControlGUI/FanCurveGraphControl.cs new file mode 100644 index 0000000..ab593e0 --- /dev/null +++ b/AsusFanControlGUI/FanCurveGraphControl.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Linq; +using System.Windows.Forms; + +namespace AsusFanControlGUI +{ + internal sealed class FanCurveGraphControl : Control + { + public const int MinimumTemperature = 20; + public const int MaximumTemperature = 110; + public const int MaximumFanSpeed = 100; + + private const int PointRadius = 6; + private const int HitRadius = 10; + + private readonly List points = new List(); + + private int draggedPointIndex = -1; + private int hoveredPointIndex = -1; + private int minimumFanSpeed; + + public FanCurveGraphControl() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true); + + BackColor = Color.White; + Size = new Size(314, 132); + MinimumFanSpeed = 0; + Curve = FanCurve.Default; + } + + public event EventHandler CurveChanged; + + public int MinimumFanSpeed + { + get => minimumFanSpeed; + set + { + var normalizedValue = Math.Max(0, Math.Min(MaximumFanSpeed, value)); + if (minimumFanSpeed == normalizedValue) + return; + + minimumFanSpeed = normalizedValue; + + if (points.Count > 0) + { + var normalizedCurve = NormalizeCurve(points); + points.Clear(); + points.AddRange(normalizedCurve.Points.Select(point => new FanCurvePoint(point.Temperature, point.FanSpeed))); + } + + Invalidate(); + } + } + + public FanCurve Curve + { + get => new FanCurve(points.Select(point => new FanCurvePoint(point.Temperature, point.FanSpeed))); + set + { + var normalizedCurve = NormalizeCurve(value?.Points ?? FanCurve.Default.Points); + points.Clear(); + points.AddRange(normalizedCurve.Points.Select(point => new FanCurvePoint(point.Temperature, point.FanSpeed))); + Invalidate(); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + + var plotRectangle = GetPlotRectangle(); + using (var backgroundBrush = new SolidBrush(Color.FromArgb(248, 250, 252))) + using (var gridPen = new Pen(Color.FromArgb(214, 222, 233))) + using (var borderPen = new Pen(Color.FromArgb(150, 164, 179))) + using (var linePen = new Pen(Color.FromArgb(49, 99, 191), 2.25f)) + using (var pointBrush = new SolidBrush(Color.FromArgb(31, 78, 161))) + using (var pointBorderPen = new Pen(Color.White, 1.5f)) + using (var highlightPen = new Pen(Color.FromArgb(255, 160, 0), 2f)) + { + e.Graphics.FillRectangle(backgroundBrush, plotRectangle); + + for (var index = 1; index < 5; index++) + { + var horizontalY = plotRectangle.Top + ((plotRectangle.Height * index) / 5.0f); + var verticalX = plotRectangle.Left + ((plotRectangle.Width * index) / 5.0f); + + e.Graphics.DrawLine(gridPen, plotRectangle.Left, horizontalY, plotRectangle.Right, horizontalY); + e.Graphics.DrawLine(gridPen, verticalX, plotRectangle.Top, verticalX, plotRectangle.Bottom); + } + + e.Graphics.DrawRectangle(borderPen, plotRectangle); + + var pathPoints = new List + { + CurveToClientPoint(MinimumTemperature, MinimumFanSpeed, plotRectangle) + }; + pathPoints.AddRange(points.Select(point => CurveToClientPoint(point.Temperature, point.FanSpeed, plotRectangle))); + pathPoints.Add(CurveToClientPoint(MaximumTemperature, MaximumFanSpeed, plotRectangle)); + + if (pathPoints.Count > 1) + e.Graphics.DrawLines(linePen, pathPoints.ToArray()); + + for (var index = 0; index < points.Count; index++) + { + var point = points[index]; + var screenPoint = CurveToClientPoint(point.Temperature, point.FanSpeed, plotRectangle); + var pointRectangle = new RectangleF(screenPoint.X - PointRadius, screenPoint.Y - PointRadius, PointRadius * 2, PointRadius * 2); + + e.Graphics.FillEllipse(pointBrush, pointRectangle); + e.Graphics.DrawEllipse(pointBorderPen, pointRectangle); + + if (index == draggedPointIndex || index == hoveredPointIndex) + e.Graphics.DrawEllipse(highlightPen, pointRectangle); + } + } + + DrawLabels(e.Graphics, plotRectangle); + DrawActivePointLabel(e.Graphics, plotRectangle); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + + if (e.Button != MouseButtons.Left) + return; + + draggedPointIndex = FindPointIndex(e.Location); + if (draggedPointIndex < 0) + return; + + Capture = true; + UpdatePointFromMouse(e.Location); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + + if (draggedPointIndex >= 0) + { + UpdatePointFromMouse(e.Location); + return; + } + + var hoveredIndex = FindPointIndex(e.Location); + if (hoveredPointIndex != hoveredIndex) + { + hoveredPointIndex = hoveredIndex; + Invalidate(); + } + + Cursor = hoveredIndex >= 0 ? Cursors.Hand : Cursors.Default; + } + + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + + if (draggedPointIndex < 0) + return; + + draggedPointIndex = -1; + Capture = false; + hoveredPointIndex = FindPointIndex(e.Location); + Cursor = hoveredPointIndex >= 0 ? Cursors.Hand : Cursors.Default; + Invalidate(); + } + + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + + if (draggedPointIndex >= 0) + return; + + hoveredPointIndex = -1; + Cursor = Cursors.Default; + Invalidate(); + } + + private void DrawLabels(Graphics graphics, Rectangle plotRectangle) + { + TextRenderer.DrawText(graphics, "Fan (%)", Font, new Point(0, 0), Color.FromArgb(58, 69, 80)); + TextRenderer.DrawText(graphics, string.Format("{0}%", MaximumFanSpeed), Font, new Point(0, plotRectangle.Top - 6), Color.FromArgb(58, 69, 80)); + TextRenderer.DrawText(graphics, string.Format("{0}%", MinimumFanSpeed), Font, new Point(0, plotRectangle.Bottom - 8), Color.FromArgb(58, 69, 80)); + TextRenderer.DrawText(graphics, string.Format("{0}C", MinimumTemperature), Font, new Point(plotRectangle.Left - 8, plotRectangle.Bottom + 6), Color.FromArgb(58, 69, 80)); + + var maximumTemperatureText = string.Format("{0}C", MaximumTemperature); + var maximumTemperatureSize = TextRenderer.MeasureText(maximumTemperatureText, Font); + TextRenderer.DrawText(graphics, maximumTemperatureText, Font, new Point(plotRectangle.Right - maximumTemperatureSize.Width + 6, plotRectangle.Bottom + 6), Color.FromArgb(58, 69, 80)); + + var axisTitle = "Temperature (C)"; + var axisTitleSize = TextRenderer.MeasureText(axisTitle, Font); + TextRenderer.DrawText(graphics, axisTitle, Font, new Point(plotRectangle.Left + ((plotRectangle.Width - axisTitleSize.Width) / 2), plotRectangle.Bottom + 6), Color.FromArgb(58, 69, 80)); + } + + private void DrawActivePointLabel(Graphics graphics, Rectangle plotRectangle) + { + var activePointIndex = draggedPointIndex >= 0 ? draggedPointIndex : hoveredPointIndex; + if (activePointIndex < 0 || activePointIndex >= points.Count) + return; + + var point = points[activePointIndex]; + var screenPoint = CurveToClientPoint(point.Temperature, point.FanSpeed, plotRectangle); + var labelText = string.Format("P{0}: {1}C / {2}%", activePointIndex + 1, point.Temperature, point.FanSpeed); + var labelSize = TextRenderer.MeasureText(labelText, Font); + var labelLocation = new Point( + Math.Max(plotRectangle.Left, Math.Min((int)screenPoint.X - (labelSize.Width / 2), plotRectangle.Right - labelSize.Width)), + Math.Max(0, (int)screenPoint.Y - labelSize.Height - 12)); + + var labelRectangle = new Rectangle(labelLocation, labelSize + new Size(8, 4)); + using (var labelBrush = new SolidBrush(Color.FromArgb(245, 248, 255))) + using (var labelBorderPen = new Pen(Color.FromArgb(182, 197, 215))) + { + graphics.FillRectangle(labelBrush, labelRectangle); + graphics.DrawRectangle(labelBorderPen, labelRectangle); + } + + TextRenderer.DrawText(graphics, labelText, Font, new Point(labelRectangle.Left + 4, labelRectangle.Top + 2), Color.FromArgb(41, 53, 65)); + } + + private Rectangle GetPlotRectangle() + { + return new Rectangle(38, 18, Math.Max(120, Width - 56), Math.Max(80, Height - 42)); + } + + private PointF CurveToClientPoint(int temperature, int fanSpeed, Rectangle plotRectangle) + { + var clampedTemperature = Math.Max(MinimumTemperature, Math.Min(MaximumTemperature, temperature)); + var clampedFanSpeed = Math.Max(MinimumFanSpeed, Math.Min(MaximumFanSpeed, fanSpeed)); + var normalizedTemperature = (clampedTemperature - MinimumTemperature) / (float)(MaximumTemperature - MinimumTemperature); + var normalizedFanSpeed = (clampedFanSpeed - MinimumFanSpeed) / (float)Math.Max(1, MaximumFanSpeed - MinimumFanSpeed); + + return new PointF( + plotRectangle.Left + (plotRectangle.Width * normalizedTemperature), + plotRectangle.Bottom - (plotRectangle.Height * normalizedFanSpeed)); + } + + private int FindPointIndex(Point clientPoint) + { + var plotRectangle = GetPlotRectangle(); + for (var index = 0; index < points.Count; index++) + { + var screenPoint = CurveToClientPoint(points[index].Temperature, points[index].FanSpeed, plotRectangle); + var deltaX = clientPoint.X - screenPoint.X; + var deltaY = clientPoint.Y - screenPoint.Y; + if ((deltaX * deltaX) + (deltaY * deltaY) <= HitRadius * HitRadius) + return index; + } + + return -1; + } + + private void UpdatePointFromMouse(Point clientPoint) + { + if (draggedPointIndex < 0 || draggedPointIndex >= points.Count) + return; + + var plotRectangle = GetPlotRectangle(); + var minimumTemperature = draggedPointIndex == 0 ? MinimumTemperature : points[draggedPointIndex - 1].Temperature + 1; + var maximumTemperature = draggedPointIndex == points.Count - 1 ? MaximumTemperature : points[draggedPointIndex + 1].Temperature - 1; + + var newTemperature = ClientToTemperature(clientPoint.X, plotRectangle); + var newFanSpeed = ClientToFanSpeed(clientPoint.Y, plotRectangle); + + newTemperature = Math.Max(minimumTemperature, Math.Min(maximumTemperature, newTemperature)); + newFanSpeed = Math.Max(MinimumFanSpeed, Math.Min(MaximumFanSpeed, newFanSpeed)); + + var existingPoint = points[draggedPointIndex]; + if (existingPoint.Temperature == newTemperature && existingPoint.FanSpeed == newFanSpeed) + return; + + points[draggedPointIndex] = new FanCurvePoint(newTemperature, newFanSpeed); + Invalidate(); + CurveChanged?.Invoke(this, EventArgs.Empty); + } + + private int ClientToTemperature(int x, Rectangle plotRectangle) + { + var normalized = (x - plotRectangle.Left) / (double)Math.Max(1, plotRectangle.Width); + return MinimumTemperature + (int)Math.Round((MaximumTemperature - MinimumTemperature) * normalized, MidpointRounding.AwayFromZero); + } + + private int ClientToFanSpeed(int y, Rectangle plotRectangle) + { + var normalized = (plotRectangle.Bottom - y) / (double)Math.Max(1, plotRectangle.Height); + return MinimumFanSpeed + (int)Math.Round((MaximumFanSpeed - MinimumFanSpeed) * normalized, MidpointRounding.AwayFromZero); + } + + private FanCurve NormalizeCurve(IEnumerable sourcePoints) + { + var rawPoints = (sourcePoints ?? FanCurve.Default.Points) + .OrderBy(point => point.Temperature) + .ToList(); + + if (rawPoints.Count != FanCurve.Default.Points.Count) + rawPoints = FanCurve.Default.Points.ToList(); + + var normalizedPoints = new List(rawPoints.Count); + for (var index = 0; index < rawPoints.Count; index++) + { + var remainingPoints = rawPoints.Count - index - 1; + var minimumTemperature = index == 0 ? MinimumTemperature : normalizedPoints[index - 1].Temperature + 1; + var maximumTemperature = MaximumTemperature - remainingPoints; + var normalizedTemperature = Math.Max(minimumTemperature, Math.Min(maximumTemperature, rawPoints[index].Temperature)); + var normalizedFanSpeed = Math.Max(MinimumFanSpeed, Math.Min(MaximumFanSpeed, rawPoints[index].FanSpeed)); + + normalizedPoints.Add(new FanCurvePoint(normalizedTemperature, normalizedFanSpeed)); + } + + return new FanCurve(normalizedPoints); + } + } +} \ No newline at end of file diff --git a/AsusFanControlGUI/Form1.cs b/AsusFanControlGUI/Form1.cs index 1678305..a030cd0 100644 --- a/AsusFanControlGUI/Form1.cs +++ b/AsusFanControlGUI/Form1.cs @@ -1,26 +1,52 @@ -using AsusFanControl; +using AsusFanControl; using System; +using System.Drawing; +using System.Linq; using System.Windows.Forms; namespace AsusFanControlGUI { public partial class Form1 : Form { - AsusControl asusControl = new AsusControl(); - int fanSpeed = 0; - Timer timer; - NotifyIcon trayIcon; - - public Form1() + private const int CurvePointCount = 5; + + private readonly AsusControl asusControl = new AsusControl(); + private readonly bool startMinimized; + private readonly CheckBox checkBoxUseTemperatureCurve = new CheckBox(); + private readonly GroupBox groupBoxTemperatureCurve = new GroupBox(); + private readonly FanCurveGraphControl fanCurveGraph = new FanCurveGraphControl(); + private readonly Label labelCurveHint = new Label(); + private ToolStripMenuItem toolStripMenuItemLaunchOnWindowsSignIn; + + private int fanSpeed = -1; + private Timer timer; + private NotifyIcon trayIcon; + private bool hardwareAccessAvailable = true; + private bool launchContextWarningShown; + private bool isLoadingSettings; + private bool isSyncingCurveEditorValues; + private bool isSyncingStartupRegistration; + + public Form1(bool startMinimized = false) { + this.startMinimized = startMinimized; + InitializeComponent(); - AppDomain.CurrentDomain.ProcessExit += new EventHandler(OnProcessExit); + InitializeDynamicControls(); + InitializeCurveEditors(); + InitializeTrayIcon(); + + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + + LoadSettingsIntoControls(); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); - toolStripMenuItemTurnOffControlOnExit.Checked = Properties.Settings.Default.turnOffControlOnExit; - toolStripMenuItemForbidUnsafeSettings.Checked = Properties.Settings.Default.forbidUnsafeSettings; - toolStripMenuItemMinimizeToTrayOnClose.Checked = Properties.Settings.Default.minimizeToTrayOnClose; - toolStripMenuItemAutoRefreshStats.Checked = Properties.Settings.Default.autoRefreshStats; - trackBarFanSpeed.Value = Properties.Settings.Default.fanSpeed; + if (startMinimized) + BeginInvoke(new Action(HideToTray)); } private void OnProcessExit(object sender, EventArgs e) @@ -31,96 +57,286 @@ private void OnProcessExit(object sender, EventArgs e) private void Form1_Load(object sender, EventArgs e) { - timerRefreshStats(); + SyncStartupRegistrationState(); + ProbeHardwareAccess(); + ApplyControlState(); + + if (Properties.Settings.Default.autoRefreshStats) + RefreshFanSpeedLabel(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (Properties.Settings.Default.minimizeToTrayOnClose && Visible) { - if(trayIcon == null) + e.Cancel = true; + HideToTray(); + return; + } + + if (trayIcon != null) + trayIcon.Visible = false; + } + + private void InitializeDynamicControls() + { + ClientSize = new Size(360, 430); + + checkBoxUseTemperatureCurve.AutoSize = true; + checkBoxUseTemperatureCurve.Location = new Point(12, 194); + checkBoxUseTemperatureCurve.Name = "checkBoxUseTemperatureCurve"; + checkBoxUseTemperatureCurve.Size = new Size(130, 17); + checkBoxUseTemperatureCurve.TabIndex = 11; + checkBoxUseTemperatureCurve.Text = "Use temperature curve"; + checkBoxUseTemperatureCurve.UseVisualStyleBackColor = true; + checkBoxUseTemperatureCurve.CheckedChanged += checkBoxUseTemperatureCurve_CheckedChanged; + + groupBoxTemperatureCurve.Location = new Point(12, 220); + groupBoxTemperatureCurve.Name = "groupBoxTemperatureCurve"; + groupBoxTemperatureCurve.Size = new Size(336, 198); + groupBoxTemperatureCurve.TabIndex = 12; + groupBoxTemperatureCurve.TabStop = false; + groupBoxTemperatureCurve.Text = "Temperature curve"; + + fanCurveGraph.Location = new Point(10, 20); + fanCurveGraph.Name = "fanCurveGraph"; + fanCurveGraph.Size = new Size(316, 140); + fanCurveGraph.TabIndex = 0; + fanCurveGraph.CurveChanged += curveEditor_ValueChanged; + + labelCurveHint.Location = new Point(10, 164); + labelCurveHint.Name = "labelCurveHint"; + labelCurveHint.Size = new Size(316, 28); + labelCurveHint.TabIndex = 1; + labelCurveHint.Text = "Drag the five points to set temperature (horizontal) and fan speed (vertical)."; + + groupBoxTemperatureCurve.Controls.Add(fanCurveGraph); + groupBoxTemperatureCurve.Controls.Add(labelCurveHint); + Controls.Add(checkBoxUseTemperatureCurve); + Controls.Add(groupBoxTemperatureCurve); + + toolStripMenuItemLaunchOnWindowsSignIn = new ToolStripMenuItem(); + toolStripMenuItemLaunchOnWindowsSignIn.CheckOnClick = true; + toolStripMenuItemLaunchOnWindowsSignIn.Name = "toolStripMenuItemLaunchOnWindowsSignIn"; + toolStripMenuItemLaunchOnWindowsSignIn.Size = new Size(207, 22); + toolStripMenuItemLaunchOnWindowsSignIn.Text = "Launch on Windows sign-in"; + toolStripMenuItemLaunchOnWindowsSignIn.CheckedChanged += toolStripMenuItemLaunchOnWindowsSignIn_CheckedChanged; + + toolStripMenuItem1.DropDownItems.Insert(0, toolStripMenuItemLaunchOnWindowsSignIn); + } + + private void InitializeCurveEditors() + { + UpdateCurveEditorValues(FanCurve.Default); + } + + private void InitializeTrayIcon() + { + trayIcon = new NotifyIcon() + { + Icon = Icon, + Text = "Asus Fan Control", + ContextMenu = new ContextMenu(new[] { - trayIcon = new NotifyIcon() + new MenuItem("Show", (sender, e) => ShowMainWindow()), + new MenuItem("Exit", (sender, e) => { - Icon = Icon, - ContextMenu = new ContextMenu(new MenuItem[] { - new MenuItem("Show", (s1, e1) => - { - trayIcon.Visible = false; - Show(); - }), - new MenuItem("Exit", (s1, e1) => - { - Close(); - trayIcon.Visible = false; - Application.Exit(); - }), - }), - }; - - trayIcon.MouseClick += (s1, e1) => - { - if (e1.Button != MouseButtons.Left) - return; - trayIcon.Visible = false; - Show(); - }; + Close(); + Application.Exit(); + }), + }), + Visible = false, + }; + + components?.Add(trayIcon); + + trayIcon.MouseClick += (sender, e) => + { + if (e.Button == MouseButtons.Left) + ShowMainWindow(); + }; + } + + private void LoadSettingsIntoControls() + { + isLoadingSettings = true; + + try + { + var settingsChanged = false; + + toolStripMenuItemTurnOffControlOnExit.Checked = Properties.Settings.Default.turnOffControlOnExit; + toolStripMenuItemForbidUnsafeSettings.Checked = Properties.Settings.Default.forbidUnsafeSettings; + toolStripMenuItemMinimizeToTrayOnClose.Checked = Properties.Settings.Default.minimizeToTrayOnClose; + toolStripMenuItemAutoRefreshStats.Checked = Properties.Settings.Default.autoRefreshStats; + + var safeManualFanSpeed = ClampFanPercentForSafety(Properties.Settings.Default.fanSpeed); + trackBarFanSpeed.Value = safeManualFanSpeed; + if (Properties.Settings.Default.fanSpeed != safeManualFanSpeed) + { + Properties.Settings.Default.fanSpeed = safeManualFanSpeed; + settingsChanged = true; } - trayIcon.Visible = true; - e.Cancel = true; - Hide(); + checkBoxTurnOn.Checked = Properties.Settings.Default.fanControlEnabled; + checkBoxUseTemperatureCurve.Checked = Properties.Settings.Default.temperatureCurveEnabled; + + fanCurveGraph.MinimumFanSpeed = GetMinimumFanSpeed(); + + var savedCurve = FanCurve.ParseOrDefault(Properties.Settings.Default.temperatureCurve); + UpdateCurveEditorValues(savedCurve); + + var normalizedCurve = fanCurveGraph.Curve; + var serializedCurve = normalizedCurve.Serialize(); + if (!string.Equals(Properties.Settings.Default.temperatureCurve, serializedCurve, StringComparison.Ordinal)) + { + Properties.Settings.Default.temperatureCurve = serializedCurve; + settingsChanged = true; + } + + if (settingsChanged) + Properties.Settings.Default.Save(); + } + finally + { + isLoadingSettings = false; + UpdateCurveUiState(); } } - private void timerRefreshStats() + private void SyncStartupRegistrationState() + { + try + { + var isEnabled = StartupRegistration.IsEnabled(); + if (isEnabled) + StartupRegistration.RefreshCurrentExecutablePath(); + + isSyncingStartupRegistration = true; + toolStripMenuItemLaunchOnWindowsSignIn.Checked = isEnabled; + } + catch (Exception ex) + { + isSyncingStartupRegistration = true; + toolStripMenuItemLaunchOnWindowsSignIn.Checked = false; + MessageBox.Show(this, ex.Message, "Startup registration error", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + finally + { + isSyncingStartupRegistration = false; + } + } + + private void ProbeHardwareAccess() + { + var fanCount = asusControl.HealthyTable_FanCounts(); + var cpuTemperature = asusControl.Thermal_Read_Cpu_Temperature(); + + hardwareAccessAvailable = fanCount > 0 || cpuTemperature > 0; + if (hardwareAccessAvailable) + return; + + labelRPM.Text = "launch via run.bat"; + labelCPUTemp.Text = "launch via run.bat"; + labelValue.Text = "launcher required"; + + if (!launchContextWarningShown) + { + launchContextWarningShown = true; + BeginInvoke(new Action(() => + MessageBox.Show(this, StartupRegistration.GetLaunchRequirementMessage(), "Launch required", MessageBoxButtons.OK, MessageBoxIcon.Warning))); + } + } + + private void ShowMainWindow() + { + trayIcon.Visible = false; + ShowInTaskbar = true; + Show(); + WindowState = FormWindowState.Normal; + Activate(); + } + + private void HideToTray() + { + trayIcon.Visible = true; + ShowInTaskbar = false; + Hide(); + } + + private void RefreshPollingTimer() { if (timer != null) { timer.Stop(); + timer.Tick -= TimerEventProcessor; + timer.Dispose(); timer = null; } - if (!Properties.Settings.Default.autoRefreshStats) + if (!hardwareAccessAvailable) + return; + + if (!Properties.Settings.Default.autoRefreshStats && !IsAutomaticCurveActive()) return; timer = new Timer(); timer.Interval = 2000; - timer.Tick += new EventHandler(TimerEventProcessor); + timer.Tick += TimerEventProcessor; timer.Start(); } private void TimerEventProcessor(object sender, EventArgs e) { - buttonRefreshRPM_Click(sender, e); - buttonRefreshCPUTemp_Click(sender, e); + RunMonitoringCycle(); } private void toolStripMenuItemTurnOffControlOnExit_CheckedChanged(object sender, EventArgs e) { + if (isLoadingSettings) + return; + Properties.Settings.Default.turnOffControlOnExit = toolStripMenuItemTurnOffControlOnExit.Checked; Properties.Settings.Default.Save(); } private void toolStripMenuItemForbidUnsafeSettings_CheckedChanged(object sender, EventArgs e) { + if (isLoadingSettings) + return; + Properties.Settings.Default.forbidUnsafeSettings = toolStripMenuItemForbidUnsafeSettings.Checked; + trackBarFanSpeed.Value = ClampFanPercentForSafety(trackBarFanSpeed.Value); + Properties.Settings.Default.fanSpeed = trackBarFanSpeed.Value; + UpdateCurveEditorMinimumFanSpeed(); + SaveCurveSettings(); Properties.Settings.Default.Save(); + + ApplyControlState(); } private void toolStripMenuItemMinimizeToTrayOnClose_Click(object sender, EventArgs e) { + if (isLoadingSettings) + return; + Properties.Settings.Default.minimizeToTrayOnClose = toolStripMenuItemMinimizeToTrayOnClose.Checked; Properties.Settings.Default.Save(); } private void toolStripMenuItemAutoRefreshStats_Click(object sender, EventArgs e) { + if (isLoadingSettings) + return; + Properties.Settings.Default.autoRefreshStats = toolStripMenuItemAutoRefreshStats.Checked; Properties.Settings.Default.Save(); - timerRefreshStats(); + RefreshPollingTimer(); + + if (Properties.Settings.Default.autoRefreshStats) + RunMonitoringCycle(); } private void toolStripMenuItemCheckForUpdates_Click(object sender, EventArgs e) @@ -128,44 +344,67 @@ private void toolStripMenuItemCheckForUpdates_Click(object sender, EventArgs e) System.Diagnostics.Process.Start("https://github.com/Karmel0x/AsusFanControl/releases"); } - private void setFanSpeed() + private void toolStripMenuItemLaunchOnWindowsSignIn_CheckedChanged(object sender, EventArgs e) { - var value = trackBarFanSpeed.Value; - Properties.Settings.Default.fanSpeed = value; - Properties.Settings.Default.Save(); + if (isLoadingSettings || isSyncingStartupRegistration) + return; - if (!checkBoxTurnOn.Checked) - value = 0; + try + { + if (toolStripMenuItemLaunchOnWindowsSignIn.Checked && !StartupRegistration.CanUseSystemLauncher(out var errorMessage)) + throw new InvalidOperationException(errorMessage); - if (value == 0) - labelValue.Text = "turned off"; - else - labelValue.Text = value.ToString(); + StartupRegistration.SetEnabled(toolStripMenuItemLaunchOnWindowsSignIn.Checked); + SyncStartupRegistrationState(); + } + catch (Exception ex) + { + MessageBox.Show(this, ex.Message, "Startup registration error", MessageBoxButtons.OK, MessageBoxIcon.Warning); + SyncStartupRegistrationState(); + } + } - if (fanSpeed == value) + private void checkBoxUseTemperatureCurve_CheckedChanged(object sender, EventArgs e) + { + if (isLoadingSettings) return; - fanSpeed = value; + Properties.Settings.Default.temperatureCurveEnabled = checkBoxUseTemperatureCurve.Checked; + Properties.Settings.Default.Save(); - asusControl.SetFanSpeeds(value); + UpdateCurveUiState(); + ApplyControlState(); + } + + private void curveEditor_ValueChanged(object sender, EventArgs e) + { + if (isLoadingSettings || isSyncingCurveEditorValues) + return; + + SaveCurveSettings(); + + if (checkBoxUseTemperatureCurve.Checked) + ApplyControlState(); } private void checkBoxTurnOn_CheckedChanged(object sender, EventArgs e) { - setFanSpeed(); + if (isLoadingSettings) + return; + + Properties.Settings.Default.fanControlEnabled = checkBoxTurnOn.Checked; + Properties.Settings.Default.Save(); + + ApplyControlState(); } private void trackBarFanSpeed_MouseCaptureChanged(object sender, EventArgs e) { - if (Properties.Settings.Default.forbidUnsafeSettings) - { - if (trackBarFanSpeed.Value < 40) - trackBarFanSpeed.Value = 40; - else if (trackBarFanSpeed.Value > 99) - trackBarFanSpeed.Value = 99; - } + trackBarFanSpeed.Value = ClampFanPercentForSafety(trackBarFanSpeed.Value); + Properties.Settings.Default.fanSpeed = trackBarFanSpeed.Value; + Properties.Settings.Default.Save(); - setFanSpeed(); + ApplyControlState(); } private void trackBarFanSpeed_KeyUp(object sender, KeyEventArgs e) @@ -178,13 +417,181 @@ private void trackBarFanSpeed_KeyUp(object sender, KeyEventArgs e) private void buttonRefreshRPM_Click(object sender, EventArgs e) { - labelRPM.Text = string.Join(" ", asusControl.GetFanSpeeds()); + RefreshFanSpeedLabel(); } private void buttonRefreshCPUTemp_Click(object sender, EventArgs e) { - labelCPUTemp.Text = $"{asusControl.Thermal_Read_Cpu_Temperature()}"; + var cpuTemperature = RefreshCpuTemperatureLabel(); + if (IsAutomaticCurveActive()) + ApplyAutomaticCurve(cpuTemperature); + } + + private void RunMonitoringCycle() + { + ulong? cpuTemperature = null; + + if (Properties.Settings.Default.autoRefreshStats || IsAutomaticCurveActive()) + cpuTemperature = RefreshCpuTemperatureLabel(); + + if (IsAutomaticCurveActive() && cpuTemperature.HasValue) + ApplyAutomaticCurve(cpuTemperature.Value); + + if (Properties.Settings.Default.autoRefreshStats) + RefreshFanSpeedLabel(); + } + + private void RefreshFanSpeedLabel() + { + if (!hardwareAccessAvailable) + { + labelRPM.Text = "launch via run.bat"; + return; + } + + labelRPM.Text = string.Join(" ", asusControl.GetFanSpeeds()); } + private ulong RefreshCpuTemperatureLabel() + { + if (!hardwareAccessAvailable) + { + labelCPUTemp.Text = "launch via run.bat"; + return 0; + } + + var cpuTemperature = asusControl.Thermal_Read_Cpu_Temperature(); + labelCPUTemp.Text = string.Format("{0} C", cpuTemperature); + return cpuTemperature; + } + + private void ApplyControlState() + { + UpdateCurveUiState(); + RefreshPollingTimer(); + + if (!hardwareAccessAvailable) + { + labelValue.Text = "launcher required"; + return; + } + + if (!checkBoxTurnOn.Checked) + { + ApplyFanSpeed(0, "turned off"); + return; + } + + if (checkBoxUseTemperatureCurve.Checked) + { + ApplyAutomaticCurve(RefreshCpuTemperatureLabel()); + return; + } + + var manualFanSpeed = ClampFanPercentForSafety(trackBarFanSpeed.Value); + trackBarFanSpeed.Value = manualFanSpeed; + ApplyFanSpeed(manualFanSpeed, string.Format("{0}%", manualFanSpeed)); + } + + private void ApplyAutomaticCurve(ulong cpuTemperature) + { + var curve = ReadCurveFromEditor(); + var targetFanSpeed = ClampFanPercentForSafety(curve.GetFanSpeed( + (int)Math.Min(cpuTemperature, (ulong)int.MaxValue), + FanCurveGraphControl.MinimumTemperature, + FanCurveGraphControl.MaximumTemperature, + GetMinimumFanSpeed(), + FanCurveGraphControl.MaximumFanSpeed)); + ApplyFanSpeed(targetFanSpeed, string.Format("{0}% (curve)", targetFanSpeed)); + } + + private void ApplyFanSpeed(int value, string labelText) + { + labelValue.Text = labelText; + + if (fanSpeed == value) + return; + + fanSpeed = value; + asusControl.SetFanSpeeds(value); + } + + private bool IsAutomaticCurveActive() + { + return checkBoxTurnOn.Checked && checkBoxUseTemperatureCurve.Checked; + } + + private void UpdateCurveUiState() + { + groupBoxTemperatureCurve.Enabled = checkBoxUseTemperatureCurve.Checked; + trackBarFanSpeed.Enabled = !checkBoxUseTemperatureCurve.Checked; + } + + private void SaveCurveSettings() + { + var curve = ReadCurveFromEditor(); + UpdateCurveEditorValues(curve); + Properties.Settings.Default.temperatureCurve = curve.Serialize(); + Properties.Settings.Default.Save(); + } + + private FanCurve ReadCurveFromEditor() + { + return fanCurveGraph.Curve; + } + + private void UpdateCurveEditorValues(FanCurve curve) + { + if (curve == null || curve.Points.Count != CurvePointCount) + curve = FanCurve.Default; + + isSyncingCurveEditorValues = true; + try + { + fanCurveGraph.MinimumFanSpeed = GetMinimumFanSpeed(); + fanCurveGraph.Curve = curve; + UpdateCurveSummaryLabel(fanCurveGraph.Curve); + } + finally + { + isSyncingCurveEditorValues = false; + } + } + + private void UpdateCurveEditorMinimumFanSpeed() + { + isSyncingCurveEditorValues = true; + try + { + fanCurveGraph.MinimumFanSpeed = GetMinimumFanSpeed(); + UpdateCurveSummaryLabel(fanCurveGraph.Curve); + } + finally + { + isSyncingCurveEditorValues = false; + } + } + + private void UpdateCurveSummaryLabel(FanCurve curve) + { + var activeCurve = curve ?? FanCurve.Default; + var summary = string.Join(" ", activeCurve.Points.Select((point, index) => + string.Format("P{0} {1}C/{2}%", index + 1, point.Temperature, point.FanSpeed))); + labelCurveHint.Text = string.Format("Drag the five points. {0}", summary); + } + + private int GetMinimumFanSpeed() + { + return Properties.Settings.Default.forbidUnsafeSettings ? 40 : 0; + } + + private int ClampFanPercentForSafety(int value) + { + var clampedValue = Math.Max(0, Math.Min(100, value)); + if (!Properties.Settings.Default.forbidUnsafeSettings) + return clampedValue; + + return Math.Max(40, Math.Min(99, clampedValue)); + } } } diff --git a/AsusFanControlGUI/Program.cs b/AsusFanControlGUI/Program.cs index 7dd3541..b40bb37 100644 --- a/AsusFanControlGUI/Program.cs +++ b/AsusFanControlGUI/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Windows.Forms; namespace AsusFanControlGUI @@ -9,11 +10,13 @@ internal static class Program /// The main entry point for the application. /// [STAThread] - static void Main() + static void Main(string[] args) { + var startMinimized = args.Any(arg => string.Equals(arg, "--minimized", StringComparison.OrdinalIgnoreCase)); + Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new Form1()); + Application.Run(new Form1(startMinimized)); } } } diff --git a/AsusFanControlGUI/Properties/AssemblyInfo.cs b/AsusFanControlGUI/Properties/AssemblyInfo.cs index fb8b54e..46b8586 100644 --- a/AsusFanControlGUI/Properties/AssemblyInfo.cs +++ b/AsusFanControlGUI/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.2.0.0")] +[assembly: AssemblyFileVersion("1.2.0.0")] diff --git a/AsusFanControlGUI/Properties/Settings.Designer.cs b/AsusFanControlGUI/Properties/Settings.Designer.cs index c660e19..8cc6d18 100644 --- a/AsusFanControlGUI/Properties/Settings.Designer.cs +++ b/AsusFanControlGUI/Properties/Settings.Designer.cs @@ -34,6 +34,18 @@ public int fanSpeed { this["fanSpeed"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool fanControlEnabled { + get { + return ((bool)(this["fanControlEnabled"])); + } + set { + this["fanControlEnabled"] = value; + } + } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] @@ -82,5 +94,29 @@ public bool autoRefreshStats { this["autoRefreshStats"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool temperatureCurveEnabled { + get { + return ((bool)(this["temperatureCurveEnabled"])); + } + set { + this["temperatureCurveEnabled"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("45:40;60:55;70:70;80:85;90:99")] + public string temperatureCurve { + get { + return ((string)(this["temperatureCurve"])); + } + set { + this["temperatureCurve"] = value; + } + } } } diff --git a/AsusFanControlGUI/Properties/Settings.settings b/AsusFanControlGUI/Properties/Settings.settings index 9d5489c..558b50e 100644 --- a/AsusFanControlGUI/Properties/Settings.settings +++ b/AsusFanControlGUI/Properties/Settings.settings @@ -5,6 +5,9 @@ 90 + + False + True @@ -17,5 +20,11 @@ False + + False + + + 45:40;60:55;70:70;80:85;90:99 + \ No newline at end of file diff --git a/AsusFanControlGUI/StartupRegistration.cs b/AsusFanControlGUI/StartupRegistration.cs new file mode 100644 index 0000000..e0bb8fc --- /dev/null +++ b/AsusFanControlGUI/StartupRegistration.cs @@ -0,0 +1,319 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace AsusFanControlGUI +{ + internal static class StartupRegistration + { + private const string LegacyRunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string LegacyValueName = "AsusFanControl"; + private const string TaskName = "AsusFanControl"; + private const string LauncherFileName = "run.bat"; + private const string PsExecFileName = "PsExec.exe"; + private const string StartMinimizedArgument = "--minimized"; + + public static bool IsEnabled() + { + return HasScheduledTask() || HasLegacyRunKeyEntry(); + } + + public static void SetEnabled(bool enabled) + { + if (enabled) + CreateOrUpdateScheduledTask(); + else + RemoveScheduledTask(); + + RemoveLegacyRunKeyEntry(); + } + + public static void RefreshCurrentExecutablePath() + { + if (!IsEnabled()) + return; + + CreateOrUpdateScheduledTask(); + RemoveLegacyRunKeyEntry(); + } + + public static bool CanUseSystemLauncher(out string errorMessage) + { + var launcherPath = GetLauncherPath(); + if (!File.Exists(launcherPath)) + { + errorMessage = string.Format("{0} was not found next to AsusFanControlGUI.exe.", LauncherFileName); + return false; + } + + if (ResolvePsExecPath() == null) + { + errorMessage = string.Format("{0} was not found next to the app or in %ProgramFiles%\\ASUS\\ASUS Fan Control.", PsExecFileName); + return false; + } + + errorMessage = null; + return true; + } + + public static string GetLaunchRequirementMessage() + { + return "This machine only responds when AsusFanControlGUI.exe is started through run.bat, which elevates and then uses PsExec to launch the GUI as SYSTEM. Launch the app through run.bat, not by opening AsusFanControlGUI.exe directly."; + } + + private static bool HasScheduledTask() + { + return RunSchtasks(string.Format("/Query /TN \"{0}\"", TaskName)).ExitCode == 0; + } + + private static void CreateOrUpdateScheduledTask() + { + if (!CanUseSystemLauncher(out var errorMessage)) + throw new InvalidOperationException(errorMessage); + + var result = RunPowerShell(BuildCreateScheduledTaskScript()); + if (result.ExitCode != 0) + throw new InvalidOperationException(string.Format("Unable to create the Windows sign-in task. {0}", result.Output).Trim()); + } + + private static void RemoveScheduledTask() + { + var result = RunSchtasks(string.Format("/Delete /TN \"{0}\" /F", TaskName)); + if (result.ExitCode != 0 && !ContainsTaskNotFoundMessage(result.Output)) + throw new InvalidOperationException(string.Format("Unable to remove the Windows sign-in task. {0}", result.Output).Trim()); + } + + private static string BuildTaskCommandLine(string launcherPath) + { + return string.Format("\"{0}\" {1}", launcherPath, StartMinimizedArgument); + } + + private static string BuildCreateScheduledTaskScript() + { + var targetUserName = EscapeForPowerShellSingleQuotedLiteral(GetTargetUserName()); + var actionArguments = EscapeForPowerShellSingleQuotedLiteral(string.Format("/c \"\"{0}\"\" {1}", GetLauncherPath(), StartMinimizedArgument)); + var taskName = EscapeForPowerShellSingleQuotedLiteral(TaskName); + + return string.Join("; ", new[] + { + string.Format("$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument '{0}'", actionArguments), + string.Format("$trigger = New-ScheduledTaskTrigger -AtLogOn -User '{0}'", targetUserName), + "$trigger.Delay = 'PT30S'", + string.Format("$principal = New-ScheduledTaskPrincipal -UserId '{0}' -LogonType Interactive -RunLevel Highest", targetUserName), + "$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable", + string.Format("Register-ScheduledTask -TaskName '{0}' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null", taskName) + }); + } + + private static string EscapeForPowerShellSingleQuotedLiteral(string value) + { + return (value ?? string.Empty).Replace("'", "''"); + } + + private static string GetLauncherPath() + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, LauncherFileName); + } + + private static string ResolvePsExecPath() + { + var localPsExecPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, PsExecFileName); + if (File.Exists(localPsExecPath)) + return localPsExecPath; + + var installedPsExecPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "ASUS", + "ASUS Fan Control", + PsExecFileName); + if (File.Exists(installedPsExecPath)) + return installedPsExecPath; + + return null; + } + + private static string GetTargetUserName() + { + var currentUserName = WindowsIdentity.GetCurrent().Name; + if (!string.Equals(currentUserName, "NT AUTHORITY\\SYSTEM", StringComparison.OrdinalIgnoreCase) && + !string.Equals(currentUserName, "SYSTEM", StringComparison.OrdinalIgnoreCase)) + { + return currentUserName; + } + + var interactiveUserName = GetActiveConsoleUserName(); + if (!string.IsNullOrWhiteSpace(interactiveUserName)) + return interactiveUserName; + + throw new InvalidOperationException("Unable to determine the interactive Windows user for startup registration."); + } + + private static bool HasLegacyRunKeyEntry() + { + return !string.IsNullOrWhiteSpace(ReadLegacyRunKeyValue()); + } + + private static void RemoveLegacyRunKeyEntry() + { + var runKeyPath = GetInteractiveUserRegistryPath(); + if (string.IsNullOrWhiteSpace(runKeyPath)) + return; + + using (var runKey = Microsoft.Win32.Registry.Users.OpenSubKey(runKeyPath, writable: true)) + { + runKey?.DeleteValue(LegacyValueName, throwOnMissingValue: false); + } + } + + private static string ReadLegacyRunKeyValue() + { + var runKeyPath = GetInteractiveUserRegistryPath(); + if (string.IsNullOrWhiteSpace(runKeyPath)) + return null; + + using (var runKey = Microsoft.Win32.Registry.Users.OpenSubKey(runKeyPath, writable: false)) + { + return runKey?.GetValue(LegacyValueName) as string; + } + } + + private static string GetInteractiveUserRegistryPath() + { + try + { + var sid = GetTargetUserSid(); + if (string.IsNullOrWhiteSpace(sid)) + return null; + + return string.Format(@"{0}\{1}", sid, LegacyRunKeyPath); + } + catch + { + return null; + } + } + + private static string GetTargetUserSid() + { + var targetUserName = GetTargetUserName(); + var ntAccount = new NTAccount(targetUserName); + var securityIdentifier = (SecurityIdentifier)ntAccount.Translate(typeof(SecurityIdentifier)); + return securityIdentifier.Value; + } + + private static bool ContainsTaskNotFoundMessage(string output) + { + return output.IndexOf("cannot find", StringComparison.OrdinalIgnoreCase) >= 0 || + output.IndexOf("cannot find the file specified", StringComparison.OrdinalIgnoreCase) >= 0 || + output.IndexOf("does not exist", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static SchtasksResult RunSchtasks(string arguments) + { + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo() + { + FileName = "schtasks.exe", + Arguments = arguments, + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + process.Start(); + var standardOutput = process.StandardOutput.ReadToEnd(); + var standardError = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + return new SchtasksResult(process.ExitCode, (standardOutput + Environment.NewLine + standardError).Trim()); + } + } + + private static SchtasksResult RunPowerShell(string command) + { + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo() + { + FileName = "powershell.exe", + Arguments = string.Format("-NoProfile -NonInteractive -Command \"{0}\"", command.Replace("\"", "\\\"")), + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + process.Start(); + var standardOutput = process.StandardOutput.ReadToEnd(); + var standardError = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + return new SchtasksResult(process.ExitCode, (standardOutput + Environment.NewLine + standardError).Trim()); + } + } + + private static string GetActiveConsoleUserName() + { + var sessionId = WTSGetActiveConsoleSessionId(); + var userName = QuerySessionInformation((int)sessionId, WtsInfoClass.WTSUserName); + if (string.IsNullOrWhiteSpace(userName)) + return null; + + var domainName = QuerySessionInformation((int)sessionId, WtsInfoClass.WTSDomainName); + if (string.IsNullOrWhiteSpace(domainName)) + return userName; + + return string.Format("{0}\\{1}", domainName, userName); + } + + private static string QuerySessionInformation(int sessionId, WtsInfoClass infoClass) + { + IntPtr buffer = IntPtr.Zero; + + try + { + if (!WTSQuerySessionInformation(IntPtr.Zero, sessionId, infoClass, out buffer, out var bytesReturned) || buffer == IntPtr.Zero || bytesReturned <= 1) + return null; + + return Marshal.PtrToStringAuto(buffer); + } + finally + { + if (buffer != IntPtr.Zero) + WTSFreeMemory(buffer); + } + } + + [DllImport("kernel32.dll")] + private static extern uint WTSGetActiveConsoleSessionId(); + + [DllImport("Wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WtsInfoClass wtsInfoClass, out IntPtr ppBuffer, out int pBytesReturned); + + [DllImport("Wtsapi32.dll")] + private static extern void WTSFreeMemory(IntPtr pMemory); + + private enum WtsInfoClass + { + WTSUserName = 5, + WTSDomainName = 7, + } + + private sealed class SchtasksResult + { + public SchtasksResult(int exitCode, string output) + { + ExitCode = exitCode; + Output = output ?? string.Empty; + } + + public int ExitCode { get; } + + public string Output { get; } + } + } +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0e1aa60 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.2.0] - 2026-04-13 + +### Added + +- Added a draggable line-graph editor for the five-point CPU temperature to fan speed curve in the GUI. + +### Fixed + +- Fixed temperature-curve settings being overwritten during startup before the saved curve was restored. + +## [1.1.0] - 2026-04-12 + +### Added + +- Added a GUI option to register Asus Fan Control to launch automatically when the current user signs in to Windows. +- Added minimized startup handling so the GUI can start from Windows sign-in without opening the main window. +- Added persisted fan control state so the GUI restores the last enabled control mode on startup. +- Added a configurable five-point CPU temperature to fan speed curve with interpolated automatic fan control. + +### Changed + +- Startup registration now uses a scheduled task that launches `run.bat` instead of starting `AsusFanControlGUI.exe` directly. +- The build now includes `run.bat`, and the GUI warns when it was started without the launcher context required by some ASUS laptops. +- Windows sign-in launch now runs in the signed-in user's interactive session instead of an inaccessible SYSTEM desktop, while still allowing minimized tray startup. diff --git a/README.md b/README.md index c132c78..421b776 100644 --- a/README.md +++ b/README.md @@ -17,19 +17,32 @@ Go to [releases](../../releases) --get-cpu-temp -GUI: `AsusFanControlGUI.exe` +GUI: `AsusFanControlGUI.exe` + +The GUI can now: + +- register itself to launch automatically when you sign in to Windows from `Advanced > Launch on Windows sign-in` by creating a scheduled task that runs `run.bat` +- restore the last enabled control mode when it starts, including starting minimized to the tray when Windows launches it +- apply a configurable five-point CPU temperature to fan speed curve with linear interpolation between points +- edit that curve by dragging points on a line graph and restore the saved curve correctly on the next launch + +Important: on systems that require ASUS fan control access through the original launcher flow, start the GUI with `run.bat`, not by opening `AsusFanControlGUI.exe` directly. The launcher elevates and uses `PsExec.exe` to start the GUI as `SYSTEM`. +Windows sign-in launch now runs in your account's interactive session and can start minimized to the tray. ![AsusFanControlGUI](https://github.com/Karmel0x/AsusFanControl/assets/25367564/fe197ad0-7079-4d51-ae78-177cb6369e96) ### Why need it? + My laptop does not support the [Fan Profile](https://github.com/Karmel0x/AsusFanControl/assets/25367564/924d990a-bf20-4b8d-bf9d-56c460174d99) option, but it often overheats. Looked for apps to control fans, but none is working. ### Compatibility + This program should work on any laptop with x64 windows where [Fan Diagnosis](https://github.com/Karmel0x/AsusFanControl/assets/25367564/7129833b-97af-4da8-9148-b71e49552ea4) in [MyASUS](https://apps.microsoft.com/store/detail/myasus/9N7R5S6B0ZZH) application is working as it is using same library. [ASUS System Control Interface](https://www.asus.com/support/faq/1047338/) is necessary for this software to work - `ASUS System Analysis` service [must be running](../../issues/16). It's automatically installed with `MyASUS` app. Included `AsusWinIO64.dll` is licenced to `(c) ASUSTek COMPUTER INC.` which can be found in `C:\Windows\System32\DriverStore\FileRepository\asussci2.inf_amd64_-\ASUSSystemAnalysis\` if you have MyASUS installed. -[Works on](../../issues/13): +[Works on](../../issues/13): + - ASUS: VivoBook, ZenBook, TUF Gaming, ROG Strix, ROG Zephyrus, ROG Flow diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..25bb7d5 --- /dev/null +++ b/run.bat @@ -0,0 +1,19 @@ +@echo off +setlocal + +set "PSEXEC=%~dp0PsExec.exe" +if not exist "%PSEXEC%" set "PSEXEC=%ProgramFiles%\ASUS\ASUS Fan Control\PsExec.exe" + +if not exist "%PSEXEC%" ( + echo PsExec.exe was not found next to run.bat or in "%ProgramFiles%\ASUS\ASUS Fan Control". + exit /b 1 +) + +net session >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo Requesting administrative privileges... + powershell -NoProfile -Command "Start-Process -FilePath 'cmd.exe' -ArgumentList '/c \"\"%~f0\"\" %*' -Verb RunAs" + exit /b +) + +"%PSEXEC%" -accepteula -i -s -d "%~dp0AsusFanControlGUI.exe" %* \ No newline at end of file