diff --git a/AsusFanControlGUI/AsusFanControlGUI.csproj b/AsusFanControlGUI/AsusFanControlGUI.csproj
index d1527a6..bd0445a 100644
--- a/AsusFanControlGUI/AsusFanControlGUI.csproj
+++ b/AsusFanControlGUI/AsusFanControlGUI.csproj
@@ -1,4 +1,4 @@
-
+
@@ -14,7 +14,8 @@
true
- AnyCPU
+ x64
+ false
true
full
false
@@ -24,7 +25,8 @@
4
- AnyCPU
+ x64
+ false
pdbonly
true
bin\Release\
@@ -65,18 +67,43 @@
+
+
Form
Form1.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Form1.cs
Designer
@@ -90,6 +117,8 @@
True
Resources.resx
+
+
SettingsSingleFileGenerator
Settings.Designer.cs
@@ -103,14 +132,14 @@
+
+
+
{df94635e-4107-4ee9-8675-7137e750bc86}
AsusFanControl
-
-
-
\ No newline at end of file
diff --git a/AsusFanControlGUI/Controls/FanCurvePanel.cs b/AsusFanControlGUI/Controls/FanCurvePanel.cs
new file mode 100644
index 0000000..a0bb0bb
--- /dev/null
+++ b/AsusFanControlGUI/Controls/FanCurvePanel.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
+using AsusFanControlGUI.Models;
+using AsusFanControlGUI.Services;
+
+namespace AsusFanControlGUI.Controls
+{
+ ///
+ /// A fully custom-painted WinForms UserControl that renders an interactive
+ /// fan-speed vs. temperature curve. Nodes are draggable when IsReadOnly = false.
+ ///
+ public class FanCurvePanel : UserControl
+ {
+ // ── Public API ───────────────────────────────────────────────────────────
+
+ private List _points = new List();
+ public List Points
+ {
+ get => _points;
+ set { _points = value ?? new List(); Invalidate(); }
+ }
+
+ public bool IsReadOnly { get; set; } = false;
+ public bool ClampToGrid { get; set; } = false;
+ public string PanelTitle { get; set; } = "Fan Profile, RPM/°C";
+ public Color CurveColor { get; set; } = Color.FromArgb(79, 195, 247);
+ public float MaxRpm { get; set; } = 7000f;
+
+ /// Fired after any node is dragged so parent can react.
+ public event EventHandler PointsChanged;
+
+ // ── Drawing constants ────────────────────────────────────────────────────
+ private const float TempMin = 20f;
+ private const float TempMax = 110f;
+ private const int NodeRadius = 6;
+ private const int ActiveNodeRadius = 8;
+ private const int HitRadius = 12;
+
+ private readonly int PadLeft = 58;
+ private readonly int PadRight = 14;
+ private readonly int PadTop = 30;
+ private readonly int PadBottom = 36;
+
+ // ── Drag state ───────────────────────────────────────────────────────────
+ private int _dragIndex = -1; // index into _points currently being dragged
+
+ // ── Constructor ──────────────────────────────────────────────────────────
+ public FanCurvePanel()
+ {
+ DoubleBuffered = true;
+ ResizeRedraw = true;
+ BackColor = Color.FromArgb(22, 22, 46);
+ MinimumSize = new Size(200, 140);
+
+ MouseDown += OnMouseDown;
+ MouseMove += OnMouseMove;
+ MouseUp += OnMouseUp;
+ }
+
+ // ── Canvas geometry ──────────────────────────────────────────────────────
+
+ private RectangleF PlotArea => new RectangleF(
+ PadLeft,
+ PadTop,
+ Math.Max(10, Width - PadLeft - PadRight),
+ Math.Max(10, Height - PadTop - PadBottom));
+
+ private PointF CurvePointToCanvas(CurvePoint pt)
+ {
+ var pa = PlotArea;
+ float xFrac = (pt.TempC - TempMin) / (TempMax - TempMin);
+ float yFrac = pt.Rpm / MaxRpm;
+ return new PointF(
+ pa.Left + xFrac * pa.Width,
+ pa.Bottom - yFrac * pa.Height); // Y inverted: high RPM = top
+ }
+
+ private CurvePoint CanvasToCurvePoint(PointF px)
+ {
+ var pa = PlotArea;
+ float xFrac = (px.X - pa.Left) / pa.Width;
+ float yFrac = (pa.Bottom - px.Y) / pa.Height;
+
+ float tempC = TempMin + xFrac * (TempMax - TempMin);
+ float rpm = yFrac * MaxRpm;
+
+ // Clamp
+ tempC = Math.Max(TempMin, Math.Min(TempMax, tempC));
+ rpm = Math.Max(0, Math.Min(MaxRpm, rpm));
+
+ if (ClampToGrid)
+ tempC = (float)(Math.Round(tempC / 5.0) * 5.0);
+
+ return new CurvePoint(tempC, rpm);
+ }
+
+ // ── Interpolation (static, usable by FanCurveDaemon too) ────────────────
+ public float GetTargetRpm(float tempC)
+ {
+ return FanCurveDaemon.InterpolateRpm(_points, tempC);
+ }
+
+ // ── Painting ─────────────────────────────────────────────────────────────
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ base.OnPaint(e);
+ var g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
+
+ var pa = PlotArea;
+
+ // Background
+ g.FillRectangle(new SolidBrush(Color.FromArgb(20, 20, 40)), 0, 0, Width, Height);
+ g.FillRectangle(new SolidBrush(Color.FromArgb(22, 22, 46)), pa.Left, pa.Top, pa.Width, pa.Height);
+
+ DrawGrid(g, pa);
+ DrawAxes(g, pa);
+ DrawCurve(g, pa);
+ DrawTitle(g);
+ }
+
+ private void DrawGrid(Graphics g, RectangleF pa)
+ {
+ using (var gridPen = new Pen(Color.FromArgb(38, 38, 60), 1f))
+ {
+ // Vertical lines every 10°C
+ for (float t = TempMin; t <= TempMax; t += 10f)
+ {
+ float x = pa.Left + (t - TempMin) / (TempMax - TempMin) * pa.Width;
+ g.DrawLine(gridPen, x, pa.Top, x, pa.Bottom);
+ }
+ // Horizontal lines every 500 RPM
+ for (float r = 0; r <= MaxRpm; r += 500f)
+ {
+ float y = pa.Bottom - r / MaxRpm * pa.Height;
+ g.DrawLine(gridPen, pa.Left, y, pa.Right, y);
+ }
+ }
+ }
+
+ private void DrawAxes(Graphics g, RectangleF pa)
+ {
+ using (var axisFont = new Font("Segoe UI", 7.5f))
+ using (var labelBrush = new SolidBrush(Color.FromArgb(140, 140, 180)))
+ using (var axisPen = new Pen(Color.FromArgb(60, 60, 90), 1.2f))
+ {
+ // Draw axis border
+ g.DrawRectangle(axisPen, pa.X, pa.Y, pa.Width, pa.Height);
+
+ // X axis labels (temperature)
+ for (float t = TempMin; t <= TempMax; t += 10f)
+ {
+ float x = pa.Left + (t - TempMin) / (TempMax - TempMin) * pa.Width;
+ string lbl = $"{(int)t}°";
+ var sz = g.MeasureString(lbl, axisFont);
+ g.DrawString(lbl, axisFont, labelBrush, x - sz.Width / 2, pa.Bottom + 4);
+ }
+ // X axis name
+ using (var boldFont = new Font("Segoe UI", 7.5f, FontStyle.Regular))
+ {
+ string xlabel = "Temperature (°C)";
+ var xsz = g.MeasureString(xlabel, boldFont);
+ g.DrawString(xlabel, boldFont, labelBrush,
+ pa.Left + (pa.Width - xsz.Width) / 2, pa.Bottom + 18);
+ }
+
+ // Y axis labels (RPM)
+ for (float r = 0; r <= MaxRpm; r += 500f)
+ {
+ float y = pa.Bottom - r / MaxRpm * pa.Height;
+ string lbl = r == 0 ? "OFF" : $"{(int)r}";
+ var sz = g.MeasureString(lbl, axisFont);
+ g.DrawString(lbl, axisFont, labelBrush,
+ pa.Left - sz.Width - 4, y - sz.Height / 2);
+ }
+ }
+ }
+
+ private void DrawCurve(Graphics g, RectangleF pa)
+ {
+ if (_points == null || _points.Count == 0) return;
+
+ // Sort by temp for drawing
+ var sorted = new List(_points);
+ sorted.Sort((a, b) => a.TempC.CompareTo(b.TempC));
+
+ // Build pixel points
+ var pixelPts = new List();
+ foreach (var pt in sorted)
+ pixelPts.Add(CurvePointToCanvas(pt));
+
+ // Draw filled area under curve
+ if (pixelPts.Count >= 2)
+ {
+ var fillPts = new List(pixelPts);
+ fillPts.Add(new PointF(pixelPts[pixelPts.Count - 1].X, pa.Bottom));
+ fillPts.Add(new PointF(pixelPts[0].X, pa.Bottom));
+ using (var fillBrush = new SolidBrush(Color.FromArgb(30,
+ CurveColor.R, CurveColor.G, CurveColor.B)))
+ {
+ g.FillPolygon(fillBrush, fillPts.ToArray());
+ }
+
+ // Draw curve line
+ using (var curvePen = new Pen(CurveColor, 2.2f) { LineJoin = LineJoin.Round })
+ {
+ g.DrawLines(curvePen, pixelPts.ToArray());
+ }
+ }
+
+ // Draw nodes
+ for (int i = 0; i < sorted.Count; i++)
+ {
+ var px = CurvePointToCanvas(sorted[i]);
+ bool isDragged = !IsReadOnly && _dragIndex >= 0 &&
+ Math.Abs(sorted[i].TempC - _points[_dragIndex].TempC) < 0.5f &&
+ Math.Abs(sorted[i].Rpm - _points[_dragIndex].Rpm) < 0.5f;
+
+ int r = isDragged ? ActiveNodeRadius : NodeRadius;
+ var nodeColor = isDragged
+ ? Color.White
+ : CurveColor;
+
+ var rect = new RectangleF(px.X - r, px.Y - r, r * 2, r * 2);
+
+ using (var fill = new SolidBrush(nodeColor))
+ g.FillEllipse(fill, rect);
+ using (var border = new Pen(Color.White, 1.2f))
+ g.DrawEllipse(border, rect);
+
+ // Tooltip: show temp + rpm values
+ using (var tipFont = new Font("Segoe UI", 7f))
+ using (var tipBrush = new SolidBrush(Color.FromArgb(200, 200, 200)))
+ {
+ string rpmLabel = sorted[i].Rpm <= 0 ? "OFF" : $"{(int)sorted[i].Rpm}";
+ string tipText = $"{(int)sorted[i].TempC}°/{rpmLabel}";
+ g.DrawString(tipText, tipFont, tipBrush, px.X + r + 2, px.Y - 8);
+ }
+ }
+ }
+
+ private void DrawTitle(Graphics g)
+ {
+ using (var titleFont = new Font("Segoe UI", 9f, FontStyle.Bold))
+ using (var titleBrush = new SolidBrush(CurveColor))
+ {
+ var sz = g.MeasureString(PanelTitle, titleFont);
+ g.DrawString(PanelTitle, titleFont, titleBrush,
+ (Width - sz.Width) / 2, 6);
+ }
+ }
+
+ // ── Mouse interaction ────────────────────────────────────────────────────
+ private void OnMouseDown(object sender, MouseEventArgs e)
+ {
+ if (IsReadOnly || e.Button != MouseButtons.Left) return;
+
+ int closest = FindNearestNode(e.Location);
+ if (closest >= 0)
+ {
+ _dragIndex = closest;
+ Cursor = Cursors.SizeAll;
+ Capture = true;
+ }
+ }
+
+ private void OnMouseMove(object sender, MouseEventArgs e)
+ {
+ if (IsReadOnly) return;
+
+ if (_dragIndex < 0)
+ {
+ // Change cursor when hovering over a node
+ Cursor = FindNearestNode(e.Location) >= 0 ? Cursors.SizeAll : Cursors.Default;
+ return;
+ }
+
+ // Move the dragged node
+ var newPt = CanvasToCurvePoint(e.Location);
+
+ // Prevent two nodes sharing the same temp (clamp to ±1°C of neighbours)
+ var sorted = new List(_points);
+ sorted.Sort((a, b) => a.TempC.CompareTo(b.TempC));
+ int sortedIdx = sorted.IndexOf(_points[_dragIndex]);
+
+ float minT = (sortedIdx > 0) ? sorted[sortedIdx - 1].TempC + 1f : TempMin;
+ float maxT = (sortedIdx < sorted.Count - 1) ? sorted[sortedIdx + 1].TempC - 1f : TempMax;
+ newPt.TempC = Math.Max(minT, Math.Min(maxT, newPt.TempC));
+
+ _points[_dragIndex] = newPt;
+ Invalidate();
+ }
+
+ private void OnMouseUp(object sender, MouseEventArgs e)
+ {
+ if (_dragIndex >= 0)
+ {
+ // Sort points by temp after drag
+ _points.Sort((a, b) => a.TempC.CompareTo(b.TempC));
+ _dragIndex = -1;
+ Cursor = Cursors.Default;
+ Capture = false;
+ Invalidate();
+ PointsChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ /// Returns the index of the nearest Point within HitRadius, or -1.
+ private int FindNearestNode(Point mousePos)
+ {
+ int best = -1;
+ float bestDist = HitRadius * HitRadius;
+ for (int i = 0; i < _points.Count; i++)
+ {
+ var px = CurvePointToCanvas(_points[i]);
+ float dx = px.X - mousePos.X;
+ float dy = px.Y - mousePos.Y;
+ float dist = dx * dx + dy * dy;
+ if (dist < bestDist)
+ {
+ bestDist = dist;
+ best = i;
+ }
+ }
+ return best;
+ }
+ }
+}
diff --git a/AsusFanControlGUI/DarkTheme.cs b/AsusFanControlGUI/DarkTheme.cs
new file mode 100644
index 0000000..0cef76d
--- /dev/null
+++ b/AsusFanControlGUI/DarkTheme.cs
@@ -0,0 +1,91 @@
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace AsusFanControlGUI
+{
+ public static class DarkTheme
+ {
+ public static Color Background = Color.FromArgb(15, 15, 26);
+ public static Color PanelBackground = Color.FromArgb(22, 22, 46);
+ public static Color GridLine = Color.FromArgb(42, 42, 62);
+ public static Color AccentBlue = Color.FromArgb(79, 195, 247);
+ public static Color AccentRed = Color.FromArgb(239, 83, 80);
+ public static Color TextPrimary = Color.White;
+ public static Color TextSecondary = Color.FromArgb(140, 140, 180);
+ public static Color ButtonBackground = Color.FromArgb(34, 34, 68);
+ public static Color ButtonHover = Color.FromArgb(51, 51, 102);
+ public static Color WarningOrange = Color.Orange;
+ public static Color ErrorRed = Color.FromArgb(239, 83, 80);
+ public static Color SuccessGreen = Color.FromArgb(102, 187, 106);
+
+ /// Recursively applies dark colors to a control and all its children.
+ public static void Apply(Control control)
+ {
+ control.BackColor = Background;
+ control.ForeColor = TextPrimary;
+
+ foreach (Control child in control.Controls)
+ {
+ if (child is Button btn)
+ {
+ StyleButton(btn);
+ }
+ else if (child is ComboBox cb)
+ {
+ StyleComboBox(cb);
+ }
+ else if (child is Label lbl)
+ {
+ lbl.BackColor = Color.Transparent;
+ lbl.ForeColor = TextPrimary;
+ }
+ else if (child is CheckBox chk)
+ {
+ chk.BackColor = Color.Transparent;
+ chk.ForeColor = TextPrimary;
+ }
+ else if (child is Panel || child is GroupBox || child is FlowLayoutPanel || child is TableLayoutPanel)
+ {
+ child.BackColor = Background;
+ child.ForeColor = TextPrimary;
+ Apply(child);
+ }
+ else if (child is TabControl tc)
+ {
+ tc.BackColor = Background;
+ tc.ForeColor = TextPrimary;
+ foreach (TabPage tp in tc.TabPages)
+ {
+ tp.BackColor = Background;
+ tp.ForeColor = TextPrimary;
+ Apply(tp);
+ }
+ }
+ else
+ {
+ Apply(child);
+ }
+ }
+ }
+
+ public static void StyleButton(Button btn)
+ {
+ btn.FlatStyle = FlatStyle.Flat;
+ btn.FlatAppearance.BorderColor = Color.FromArgb(60, 60, 100);
+ btn.FlatAppearance.BorderSize = 1;
+ btn.BackColor = ButtonBackground;
+ btn.ForeColor = TextPrimary;
+ btn.Cursor = Cursors.Hand;
+
+ btn.MouseEnter += (s, e) => btn.BackColor = ButtonHover;
+ btn.MouseLeave += (s, e) => btn.BackColor = ButtonBackground;
+ }
+
+ public static void StyleComboBox(ComboBox cb)
+ {
+ cb.BackColor = ButtonBackground;
+ cb.ForeColor = TextPrimary;
+ cb.FlatStyle = FlatStyle.Flat;
+ }
+ }
+}
diff --git a/AsusFanControlGUI/ElevationHelper.cs b/AsusFanControlGUI/ElevationHelper.cs
new file mode 100644
index 0000000..702bd71
--- /dev/null
+++ b/AsusFanControlGUI/ElevationHelper.cs
@@ -0,0 +1,31 @@
+using System.Security.Principal;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace AsusFanControlGUI
+{
+ public static class ElevationHelper
+ {
+ public static bool IsRunningAsSystem()
+ {
+ using (var identity = WindowsIdentity.GetCurrent())
+ {
+ return identity.IsSystem;
+ }
+ }
+
+ public static void ShowElevationWarning(Label statusLabel)
+ {
+ if (!IsRunningAsSystem())
+ {
+ statusLabel.ForeColor = DarkTheme.WarningOrange;
+ statusLabel.Text = "Warning: Not running as SYSTEM. Fan control may fail on driver v3.1.41.0+. Use run.bat.";
+ }
+ else
+ {
+ statusLabel.ForeColor = DarkTheme.SuccessGreen;
+ statusLabel.Text = "Status: Ready (running as SYSTEM)";
+ }
+ }
+ }
+}
diff --git a/AsusFanControlGUI/Form1.Designer.cs b/AsusFanControlGUI/Form1.Designer.cs
index e1b5de3..b73782f 100644
--- a/AsusFanControlGUI/Form1.Designer.cs
+++ b/AsusFanControlGUI/Form1.Designer.cs
@@ -1,227 +1,367 @@
-namespace AsusFanControlGUI
+using AsusFanControlGUI.Controls;
+
+namespace AsusFanControlGUI
{
partial class Form1
{
- ///
- /// Required designer variable.
- ///
private System.ComponentModel.IContainer components = null;
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
- {
components.Dispose();
- }
base.Dispose(disposing);
}
#region Windows Form Designer generated code
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
private void InitializeComponent()
{
- System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
- this.trackBarFanSpeed = new System.Windows.Forms.TrackBar();
- this.label1 = new System.Windows.Forms.Label();
- this.labelValue = new System.Windows.Forms.Label();
- this.label2 = new System.Windows.Forms.Label();
- this.buttonRefreshRPM = new System.Windows.Forms.Button();
- this.labelRPM = new System.Windows.Forms.Label();
- this.checkBoxTurnOn = new System.Windows.Forms.CheckBox();
- this.labelCPUTemp = new System.Windows.Forms.Label();
- this.buttonRefreshCPUTemp = new System.Windows.Forms.Button();
- this.label4 = new System.Windows.Forms.Label();
- this.menuStrip1 = new System.Windows.Forms.MenuStrip();
- this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
- this.toolStripMenuItemTurnOffControlOnExit = new System.Windows.Forms.ToolStripMenuItem();
- this.toolStripMenuItemForbidUnsafeSettings = new System.Windows.Forms.ToolStripMenuItem();
+ System.ComponentModel.ComponentResourceManager resources =
+ new System.ComponentModel.ComponentResourceManager(typeof(Form1));
+
+ // ── Declare all controls ──────────────────────────────────────────────
+ this.menuStrip1 = new System.Windows.Forms.MenuStrip();
+ this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripMenuItemTurnOffControlOnExit = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripMenuItemForbidUnsafeSettings = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripMenuItemMinimizeToTrayOnClose = new System.Windows.Forms.ToolStripMenuItem();
- this.toolStripMenuItemAutoRefreshStats = new System.Windows.Forms.ToolStripMenuItem();
- this.toolStripMenuItemCheckForUpdates = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripMenuItemAutoRefreshStats = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripMenuItemCheckForUpdates = new System.Windows.Forms.ToolStripMenuItem();
+
+ this.mainTabControl = new System.Windows.Forms.TabControl();
+ this.tabPageManual = new System.Windows.Forms.TabPage();
+ this.tabPageFanCurves = new System.Windows.Forms.TabPage();
+
+ // Manual tab controls
+ this.trackBarFanSpeed = new System.Windows.Forms.TrackBar();
+ this.label1 = new System.Windows.Forms.Label();
+ this.labelValue = new System.Windows.Forms.Label();
+ this.label2 = new System.Windows.Forms.Label();
+ this.buttonRefreshRPM = new System.Windows.Forms.Button();
+ this.labelRPM = new System.Windows.Forms.Label();
+ this.checkBoxTurnOn = new System.Windows.Forms.CheckBox();
+ this.labelCPUTemp = new System.Windows.Forms.Label();
+ this.buttonRefreshCPUTemp = new System.Windows.Forms.Button();
+ this.label4 = new System.Windows.Forms.Label();
+
+ // Fan Curves tab controls
+ this.panelCurveToolbar = new System.Windows.Forms.FlowLayoutPanel();
+ this.labelProfile = new System.Windows.Forms.Label();
+ this.profileComboBox = new System.Windows.Forms.ComboBox();
+ this.btnSaveProfile = new System.Windows.Forms.Button();
+ this.btnNewProfile = new System.Windows.Forms.Button();
+ this.btnDeleteProfile = new System.Windows.Forms.Button();
+ this.checkBoxClampToGrid = new System.Windows.Forms.CheckBox();
+
+ this.fanCurvePanelCpu = new FanCurvePanel();
+ this.fanCurvePanelGpu = new FanCurvePanel();
+
+ this.panelCurveBottom = new System.Windows.Forms.Panel();
+ this.btnStartCurve = new System.Windows.Forms.Button();
+ this.btnStopCurve = new System.Windows.Forms.Button();
+ this.lblCurveStatus = new System.Windows.Forms.Label();
+ this.lblLiveCpuTemp = new System.Windows.Forms.Label();
+ this.lblLiveRpm = new System.Windows.Forms.Label();
+
+ // ── Suspend layouts ───────────────────────────────────────────────────
((System.ComponentModel.ISupportInitialize)(this.trackBarFanSpeed)).BeginInit();
this.menuStrip1.SuspendLayout();
+ this.mainTabControl.SuspendLayout();
+ this.tabPageManual.SuspendLayout();
+ this.tabPageFanCurves.SuspendLayout();
+ this.panelCurveToolbar.SuspendLayout();
+ this.panelCurveBottom.SuspendLayout();
this.SuspendLayout();
- //
- // trackBarFanSpeed
- //
- this.trackBarFanSpeed.Location = new System.Drawing.Point(12, 62);
+
+ // ════════════════════════════════════════════════════════════════════
+ // menuStrip1
+ // ════════════════════════════════════════════════════════════════════
+ this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[]
+ {
+ this.toolStripMenuItem1,
+ this.toolStripMenuItemCheckForUpdates
+ });
+ this.menuStrip1.Location = new System.Drawing.Point(0, 0);
+ this.menuStrip1.Name = "menuStrip1";
+ this.menuStrip1.Size = new System.Drawing.Size(760, 24);
+ this.menuStrip1.TabIndex = 0;
+
+ // toolStripMenuItem1 (Advanced)
+ this.toolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[]
+ {
+ this.toolStripMenuItemTurnOffControlOnExit,
+ this.toolStripMenuItemForbidUnsafeSettings,
+ this.toolStripMenuItemMinimizeToTrayOnClose,
+ this.toolStripMenuItemAutoRefreshStats
+ });
+ this.toolStripMenuItem1.Name = "toolStripMenuItem1";
+ this.toolStripMenuItem1.Text = "Advanced";
+
+ this.toolStripMenuItemTurnOffControlOnExit.CheckOnClick = true;
+ this.toolStripMenuItemTurnOffControlOnExit.Name = "toolStripMenuItemTurnOffControlOnExit";
+ this.toolStripMenuItemTurnOffControlOnExit.Text = "Turn off control on exit";
+ this.toolStripMenuItemTurnOffControlOnExit.CheckedChanged +=
+ new System.EventHandler(this.toolStripMenuItemTurnOffControlOnExit_CheckedChanged);
+
+ this.toolStripMenuItemForbidUnsafeSettings.CheckOnClick = true;
+ this.toolStripMenuItemForbidUnsafeSettings.Name = "toolStripMenuItemForbidUnsafeSettings";
+ this.toolStripMenuItemForbidUnsafeSettings.Text = "Forbid unsafe settings";
+ this.toolStripMenuItemForbidUnsafeSettings.CheckedChanged +=
+ new System.EventHandler(this.toolStripMenuItemForbidUnsafeSettings_CheckedChanged);
+
+ this.toolStripMenuItemMinimizeToTrayOnClose.CheckOnClick = true;
+ this.toolStripMenuItemMinimizeToTrayOnClose.Name = "toolStripMenuItemMinimizeToTrayOnClose";
+ this.toolStripMenuItemMinimizeToTrayOnClose.Text = "Minimize to tray on close";
+ this.toolStripMenuItemMinimizeToTrayOnClose.Click +=
+ new System.EventHandler(this.toolStripMenuItemMinimizeToTrayOnClose_Click);
+
+ this.toolStripMenuItemAutoRefreshStats.CheckOnClick = true;
+ this.toolStripMenuItemAutoRefreshStats.Name = "toolStripMenuItemAutoRefreshStats";
+ this.toolStripMenuItemAutoRefreshStats.Text = "Auto refresh stats";
+ this.toolStripMenuItemAutoRefreshStats.Click +=
+ new System.EventHandler(this.toolStripMenuItemAutoRefreshStats_Click);
+
+ this.toolStripMenuItemCheckForUpdates.Name = "toolStripMenuItemCheckForUpdates";
+ this.toolStripMenuItemCheckForUpdates.Text = "Check for updates";
+ this.toolStripMenuItemCheckForUpdates.Click +=
+ new System.EventHandler(this.toolStripMenuItemCheckForUpdates_Click);
+
+ // ════════════════════════════════════════════════════════════════════
+ // mainTabControl
+ // ════════════════════════════════════════════════════════════════════
+ this.mainTabControl.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.mainTabControl.Name = "mainTabControl";
+ this.mainTabControl.SelectedIndex = 0;
+ this.mainTabControl.TabPages.AddRange(new System.Windows.Forms.TabPage[]
+ {
+ this.tabPageManual,
+ this.tabPageFanCurves
+ });
+ this.mainTabControl.SelectedIndexChanged +=
+ new System.EventHandler(this.mainTabControl_SelectedIndexChanged);
+
+ // ════════════════════════════════════════════════════════════════════
+ // tabPageManual — existing controls repositioned inside tab
+ // ════════════════════════════════════════════════════════════════════
+ this.tabPageManual.Name = "tabPageManual";
+ this.tabPageManual.Text = "Manual Control";
+ this.tabPageManual.UseVisualStyleBackColor = false;
+ this.tabPageManual.Controls.AddRange(new System.Windows.Forms.Control[]
+ {
+ this.checkBoxTurnOn,
+ this.trackBarFanSpeed,
+ this.label1,
+ this.labelValue,
+ this.label2,
+ this.buttonRefreshRPM,
+ this.labelRPM,
+ this.buttonRefreshCPUTemp,
+ this.label4,
+ this.labelCPUTemp
+ });
+
+ // Manual tab controls
+ this.checkBoxTurnOn.AutoSize = true;
+ this.checkBoxTurnOn.Location = new System.Drawing.Point(12, 14);
+ this.checkBoxTurnOn.Name = "checkBoxTurnOn";
+ this.checkBoxTurnOn.Text = "Turn on fan control";
+ this.checkBoxTurnOn.CheckedChanged +=
+ new System.EventHandler(this.checkBoxTurnOn_CheckedChanged);
+
+ this.trackBarFanSpeed.Location = new System.Drawing.Point(12, 38);
this.trackBarFanSpeed.Maximum = 100;
this.trackBarFanSpeed.Name = "trackBarFanSpeed";
this.trackBarFanSpeed.Size = new System.Drawing.Size(300, 45);
- this.trackBarFanSpeed.TabIndex = 0;
this.trackBarFanSpeed.Value = 100;
- this.trackBarFanSpeed.KeyUp += new System.Windows.Forms.KeyEventHandler(this.trackBarFanSpeed_KeyUp);
- this.trackBarFanSpeed.MouseCaptureChanged += new System.EventHandler(this.trackBarFanSpeed_MouseCaptureChanged);
- //
- // label1
- //
+ this.trackBarFanSpeed.KeyUp +=
+ new System.Windows.Forms.KeyEventHandler(this.trackBarFanSpeed_KeyUp);
+ this.trackBarFanSpeed.MouseCaptureChanged +=
+ new System.EventHandler(this.trackBarFanSpeed_MouseCaptureChanged);
+
this.label1.AutoSize = true;
- this.label1.Location = new System.Drawing.Point(12, 110);
- this.label1.Name = "label1";
- this.label1.Size = new System.Drawing.Size(73, 13);
- this.label1.TabIndex = 1;
+ this.label1.Location = new System.Drawing.Point(12, 88);
this.label1.Text = "Current value:";
- //
- // labelValue
- //
+
this.labelValue.AutoSize = true;
- this.labelValue.Location = new System.Drawing.Point(91, 110);
- this.labelValue.Name = "labelValue";
- this.labelValue.Size = new System.Drawing.Size(10, 13);
- this.labelValue.TabIndex = 2;
+ this.labelValue.Location = new System.Drawing.Point(95, 88);
this.labelValue.Text = "-";
- //
- // label2
- //
+
this.label2.AutoSize = true;
- this.label2.Location = new System.Drawing.Point(40, 139);
- this.label2.Name = "label2";
- this.label2.Size = new System.Drawing.Size(71, 13);
- this.label2.TabIndex = 3;
+ this.label2.Location = new System.Drawing.Point(40, 116);
this.label2.Text = "Current RPM:";
- //
- // buttonRefreshRPM
- //
- this.buttonRefreshRPM.Location = new System.Drawing.Point(12, 134);
+
+ this.buttonRefreshRPM.Location = new System.Drawing.Point(12, 111);
this.buttonRefreshRPM.Name = "buttonRefreshRPM";
this.buttonRefreshRPM.Size = new System.Drawing.Size(22, 23);
- this.buttonRefreshRPM.TabIndex = 4;
this.buttonRefreshRPM.Text = "↻";
this.buttonRefreshRPM.UseVisualStyleBackColor = true;
- this.buttonRefreshRPM.Click += new System.EventHandler(this.buttonRefreshRPM_Click);
- //
- // labelRPM
- //
+ this.buttonRefreshRPM.Click +=
+ new System.EventHandler(this.buttonRefreshRPM_Click);
+
this.labelRPM.AutoSize = true;
- this.labelRPM.Location = new System.Drawing.Point(117, 139);
- this.labelRPM.Name = "labelRPM";
- this.labelRPM.Size = new System.Drawing.Size(10, 13);
- this.labelRPM.TabIndex = 5;
+ this.labelRPM.Location = new System.Drawing.Point(120, 116);
this.labelRPM.Text = "-";
- //
- // checkBoxTurnOn
- //
- this.checkBoxTurnOn.AutoSize = true;
- this.checkBoxTurnOn.Location = new System.Drawing.Point(12, 37);
- this.checkBoxTurnOn.Name = "checkBoxTurnOn";
- this.checkBoxTurnOn.Size = new System.Drawing.Size(116, 17);
- this.checkBoxTurnOn.TabIndex = 6;
- this.checkBoxTurnOn.Text = "Turn on fan control";
- this.checkBoxTurnOn.UseVisualStyleBackColor = true;
- this.checkBoxTurnOn.CheckedChanged += new System.EventHandler(this.checkBoxTurnOn_CheckedChanged);
- //
- // labelCPUTemp
- //
- this.labelCPUTemp.AutoSize = true;
- this.labelCPUTemp.Location = new System.Drawing.Point(141, 168);
- this.labelCPUTemp.Name = "labelCPUTemp";
- this.labelCPUTemp.Size = new System.Drawing.Size(10, 13);
- this.labelCPUTemp.TabIndex = 9;
- this.labelCPUTemp.Text = "-";
- //
- // buttonRefreshCPUTemp
- //
- this.buttonRefreshCPUTemp.Location = new System.Drawing.Point(12, 163);
+
+ this.buttonRefreshCPUTemp.Location = new System.Drawing.Point(12, 140);
this.buttonRefreshCPUTemp.Name = "buttonRefreshCPUTemp";
this.buttonRefreshCPUTemp.Size = new System.Drawing.Size(22, 23);
- this.buttonRefreshCPUTemp.TabIndex = 8;
this.buttonRefreshCPUTemp.Text = "↻";
this.buttonRefreshCPUTemp.UseVisualStyleBackColor = true;
- this.buttonRefreshCPUTemp.Click += new System.EventHandler(this.buttonRefreshCPUTemp_Click);
- //
- // label4
- //
+ this.buttonRefreshCPUTemp.Click +=
+ new System.EventHandler(this.buttonRefreshCPUTemp_Click);
+
this.label4.AutoSize = true;
- this.label4.Location = new System.Drawing.Point(40, 168);
- this.label4.Name = "label4";
- this.label4.Size = new System.Drawing.Size(95, 13);
- this.label4.TabIndex = 7;
+ this.label4.Location = new System.Drawing.Point(40, 145);
this.label4.Text = "Current CPU temp:";
- //
- // menuStrip1
- //
- this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.toolStripMenuItem1,
- this.toolStripMenuItemCheckForUpdates});
- this.menuStrip1.Location = new System.Drawing.Point(0, 0);
- this.menuStrip1.Name = "menuStrip1";
- this.menuStrip1.Size = new System.Drawing.Size(324, 24);
- this.menuStrip1.TabIndex = 10;
- this.menuStrip1.Text = "menuStrip1";
- //
- // toolStripMenuItem1
- //
- this.toolStripMenuItem1.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.toolStripMenuItemTurnOffControlOnExit,
- this.toolStripMenuItemForbidUnsafeSettings,
- this.toolStripMenuItemMinimizeToTrayOnClose,
- this.toolStripMenuItemAutoRefreshStats});
- this.toolStripMenuItem1.Name = "toolStripMenuItem1";
- this.toolStripMenuItem1.Size = new System.Drawing.Size(72, 20);
- this.toolStripMenuItem1.Text = "Advanced";
- //
- // toolStripMenuItemTurnOffControlOnExit
- //
- this.toolStripMenuItemTurnOffControlOnExit.CheckOnClick = true;
- this.toolStripMenuItemTurnOffControlOnExit.Name = "toolStripMenuItemTurnOffControlOnExit";
- this.toolStripMenuItemTurnOffControlOnExit.Size = new System.Drawing.Size(207, 22);
- this.toolStripMenuItemTurnOffControlOnExit.Text = "Turn off control on exit";
- this.toolStripMenuItemTurnOffControlOnExit.CheckedChanged += new System.EventHandler(this.toolStripMenuItemTurnOffControlOnExit_CheckedChanged);
- //
- // toolStripMenuItemForbidUnsafeSettings
- //
- this.toolStripMenuItemForbidUnsafeSettings.CheckOnClick = true;
- this.toolStripMenuItemForbidUnsafeSettings.Name = "toolStripMenuItemForbidUnsafeSettings";
- this.toolStripMenuItemForbidUnsafeSettings.Size = new System.Drawing.Size(207, 22);
- this.toolStripMenuItemForbidUnsafeSettings.Text = "Forbid unsafe settings";
- this.toolStripMenuItemForbidUnsafeSettings.CheckedChanged += new System.EventHandler(this.toolStripMenuItemForbidUnsafeSettings_CheckedChanged);
- //
- // toolStripMenuItemMinimizeToTrayOnClose
- //
- this.toolStripMenuItemMinimizeToTrayOnClose.CheckOnClick = true;
- this.toolStripMenuItemMinimizeToTrayOnClose.Name = "toolStripMenuItemMinimizeToTrayOnClose";
- this.toolStripMenuItemMinimizeToTrayOnClose.Size = new System.Drawing.Size(207, 22);
- this.toolStripMenuItemMinimizeToTrayOnClose.Text = "Minimize to tray on close";
- this.toolStripMenuItemMinimizeToTrayOnClose.Click += new System.EventHandler(this.toolStripMenuItemMinimizeToTrayOnClose_Click);
- //
- // toolStripMenuItemAutoRefreshStats
- //
- this.toolStripMenuItemAutoRefreshStats.CheckOnClick = true;
- this.toolStripMenuItemAutoRefreshStats.Name = "toolStripMenuItemAutoRefreshStats";
- this.toolStripMenuItemAutoRefreshStats.Size = new System.Drawing.Size(207, 22);
- this.toolStripMenuItemAutoRefreshStats.Text = "Auto refresh stats";
- this.toolStripMenuItemAutoRefreshStats.Click += new System.EventHandler(this.toolStripMenuItemAutoRefreshStats_Click);
- //
- // toolStripMenuItemCheckForUpdates
- //
- this.toolStripMenuItemCheckForUpdates.Name = "toolStripMenuItemCheckForUpdates";
- this.toolStripMenuItemCheckForUpdates.Size = new System.Drawing.Size(115, 20);
- this.toolStripMenuItemCheckForUpdates.Text = "Check for updates";
- this.toolStripMenuItemCheckForUpdates.Click += new System.EventHandler(this.toolStripMenuItemCheckForUpdates_Click);
- //
- // Form1
- //
+
+ this.labelCPUTemp.AutoSize = true;
+ this.labelCPUTemp.Location = new System.Drawing.Point(148, 145);
+ this.labelCPUTemp.Text = "-";
+
+ // ════════════════════════════════════════════════════════════════════
+ // tabPageFanCurves
+ // ════════════════════════════════════════════════════════════════════
+ this.tabPageFanCurves.Name = "tabPageFanCurves";
+ this.tabPageFanCurves.Text = "Fan Curves";
+ this.tabPageFanCurves.UseVisualStyleBackColor = false;
+ this.tabPageFanCurves.Padding = new System.Windows.Forms.Padding(4);
+ this.tabPageFanCurves.Controls.AddRange(new System.Windows.Forms.Control[]
+ {
+ this.panelCurveToolbar,
+ this.fanCurvePanelCpu,
+ this.fanCurvePanelGpu,
+ this.panelCurveBottom
+ });
+
+ // ── Toolbar (FlowLayoutPanel) ─────────────────────────────────────────
+ this.panelCurveToolbar.Dock = System.Windows.Forms.DockStyle.Top;
+ this.panelCurveToolbar.FlowDirection = System.Windows.Forms.FlowDirection.LeftToRight;
+ this.panelCurveToolbar.WrapContents = false;
+ this.panelCurveToolbar.Height = 38;
+ this.panelCurveToolbar.Padding = new System.Windows.Forms.Padding(4, 4, 4, 0);
+ this.panelCurveToolbar.Name = "panelCurveToolbar";
+ this.panelCurveToolbar.Controls.AddRange(new System.Windows.Forms.Control[]
+ {
+ this.labelProfile,
+ this.profileComboBox,
+ this.btnSaveProfile,
+ this.btnNewProfile,
+ this.btnDeleteProfile,
+ this.checkBoxClampToGrid
+ });
+
+ this.labelProfile.Text = "Profile:";
+ this.labelProfile.AutoSize = true;
+ this.labelProfile.Margin = new System.Windows.Forms.Padding(0, 7, 4, 0);
+
+ this.profileComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.profileComboBox.DisplayMember = "Name";
+ this.profileComboBox.Width = 160;
+ this.profileComboBox.Margin = new System.Windows.Forms.Padding(0, 4, 6, 0);
+ this.profileComboBox.Name = "profileComboBox";
+ this.profileComboBox.SelectedIndexChanged +=
+ new System.EventHandler(this.profileComboBox_SelectedIndexChanged);
+
+ this.btnSaveProfile.Text = "💾 Save";
+ this.btnSaveProfile.Width = 68;
+ this.btnSaveProfile.Height = 26;
+ this.btnSaveProfile.Margin = new System.Windows.Forms.Padding(0, 4, 4, 0);
+ this.btnSaveProfile.Name = "btnSaveProfile";
+ this.btnSaveProfile.Enabled = false;
+ this.btnSaveProfile.Click += new System.EventHandler(this.btnSaveProfile_Click);
+
+ this.btnNewProfile.Text = "+ New";
+ this.btnNewProfile.Width = 64;
+ this.btnNewProfile.Height = 26;
+ this.btnNewProfile.Margin = new System.Windows.Forms.Padding(0, 4, 4, 0);
+ this.btnNewProfile.Name = "btnNewProfile";
+ this.btnNewProfile.Click += new System.EventHandler(this.btnNewProfile_Click);
+
+ this.btnDeleteProfile.Text = "🗑 Delete";
+ this.btnDeleteProfile.Width = 74;
+ this.btnDeleteProfile.Height = 26;
+ this.btnDeleteProfile.Margin = new System.Windows.Forms.Padding(0, 4, 8, 0);
+ this.btnDeleteProfile.Name = "btnDeleteProfile";
+ this.btnDeleteProfile.Enabled = false;
+ this.btnDeleteProfile.Click += new System.EventHandler(this.btnDeleteProfile_Click);
+
+ this.checkBoxClampToGrid.Text = "Clamp to Grid (5°C)";
+ this.checkBoxClampToGrid.AutoSize = true;
+ this.checkBoxClampToGrid.Margin = new System.Windows.Forms.Padding(0, 7, 0, 0);
+ this.checkBoxClampToGrid.Name = "checkBoxClampToGrid";
+ this.checkBoxClampToGrid.CheckedChanged +=
+ new System.EventHandler(this.checkBoxClampToGrid_CheckedChanged);
+
+ // ── CPU Fan Curve Panel ───────────────────────────────────────────────
+ this.fanCurvePanelCpu.Dock = System.Windows.Forms.DockStyle.Top;
+ this.fanCurvePanelCpu.Height = 210;
+ this.fanCurvePanelCpu.Name = "fanCurvePanelCpu";
+ this.fanCurvePanelCpu.PanelTitle = "CPU Fan Profile, RPM/°C";
+ this.fanCurvePanelCpu.CurveColor = System.Drawing.Color.FromArgb(79, 195, 247);
+ this.fanCurvePanelCpu.Margin = new System.Windows.Forms.Padding(0, 2, 0, 2);
+
+ // ── GPU Fan Curve Panel ───────────────────────────────────────────────
+ this.fanCurvePanelGpu.Dock = System.Windows.Forms.DockStyle.Top;
+ this.fanCurvePanelGpu.Height = 210;
+ this.fanCurvePanelGpu.Name = "fanCurvePanelGpu";
+ this.fanCurvePanelGpu.PanelTitle = "GPU Fan Profile, RPM/°C";
+ this.fanCurvePanelGpu.CurveColor = System.Drawing.Color.FromArgb(239, 83, 80);
+ this.fanCurvePanelGpu.Margin = new System.Windows.Forms.Padding(0, 2, 0, 2);
+
+ // ── Bottom control bar ────────────────────────────────────────────────
+ this.panelCurveBottom.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.panelCurveBottom.Height = 38;
+ this.panelCurveBottom.Name = "panelCurveBottom";
+ this.panelCurveBottom.Controls.AddRange(new System.Windows.Forms.Control[]
+ {
+ this.btnStartCurve,
+ this.btnStopCurve,
+ this.lblCurveStatus,
+ this.lblLiveCpuTemp,
+ this.lblLiveRpm
+ });
+
+ this.btnStartCurve.Text = "▶ Start Curve Control";
+ this.btnStartCurve.Location = new System.Drawing.Point(6, 6);
+ this.btnStartCurve.Size = new System.Drawing.Size(148, 26);
+ this.btnStartCurve.Name = "btnStartCurve";
+ this.btnStartCurve.Click += new System.EventHandler(this.btnStartCurve_Click);
+
+ this.btnStopCurve.Text = "■ Stop";
+ this.btnStopCurve.Location = new System.Drawing.Point(160, 6);
+ this.btnStopCurve.Size = new System.Drawing.Size(72, 26);
+ this.btnStopCurve.Name = "btnStopCurve";
+ this.btnStopCurve.Enabled = false;
+ this.btnStopCurve.Click += new System.EventHandler(this.btnStopCurve_Click);
+
+ this.lblCurveStatus.AutoSize = false;
+ this.lblCurveStatus.Location = new System.Drawing.Point(240, 10);
+ this.lblCurveStatus.Size = new System.Drawing.Size(260, 18);
+ this.lblCurveStatus.Name = "lblCurveStatus";
+ this.lblCurveStatus.Text = "Status: Idle";
+ this.lblCurveStatus.ForeColor = System.Drawing.Color.FromArgb(140, 140, 180);
+
+ this.lblLiveCpuTemp.AutoSize = true;
+ this.lblLiveCpuTemp.Location = new System.Drawing.Point(508, 10);
+ this.lblLiveCpuTemp.Name = "lblLiveCpuTemp";
+ this.lblLiveCpuTemp.Text = "CPU: --°C";
+ this.lblLiveCpuTemp.ForeColor = System.Drawing.Color.FromArgb(79, 195, 247);
+
+ this.lblLiveRpm.AutoSize = true;
+ this.lblLiveRpm.Location = new System.Drawing.Point(594, 10);
+ this.lblLiveRpm.Name = "lblLiveRpm";
+ this.lblLiveRpm.Text = "Fan: --%";
+ this.lblLiveRpm.ForeColor = System.Drawing.Color.FromArgb(239, 83, 80);
+
+ // ════════════════════════════════════════════════════════════════════
+ // Form1
+ // ════════════════════════════════════════════════════════════════════
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(324, 198);
- this.Controls.Add(this.labelCPUTemp);
- this.Controls.Add(this.buttonRefreshCPUTemp);
- this.Controls.Add(this.label4);
- this.Controls.Add(this.checkBoxTurnOn);
- this.Controls.Add(this.labelRPM);
- this.Controls.Add(this.buttonRefreshRPM);
- this.Controls.Add(this.label2);
- this.Controls.Add(this.labelValue);
- this.Controls.Add(this.label1);
- this.Controls.Add(this.trackBarFanSpeed);
+ this.ClientSize = new System.Drawing.Size(760, 540);
+ this.MinimumSize = new System.Drawing.Size(760, 540);
+ this.Controls.Add(this.mainTabControl);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
@@ -229,16 +369,39 @@ private void InitializeComponent()
this.Text = "Asus Fan Control";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
this.Load += new System.EventHandler(this.Form1_Load);
+
+ // ── Resume layouts ────────────────────────────────────────────────────
((System.ComponentModel.ISupportInitialize)(this.trackBarFanSpeed)).EndInit();
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
+ this.panelCurveToolbar.ResumeLayout(false);
+ this.panelCurveToolbar.PerformLayout();
+ this.panelCurveBottom.ResumeLayout(false);
+ this.panelCurveBottom.PerformLayout();
+ this.tabPageManual.ResumeLayout(false);
+ this.tabPageManual.PerformLayout();
+ this.tabPageFanCurves.ResumeLayout(false);
+ this.mainTabControl.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
-
}
#endregion
+ // ── Field declarations ────────────────────────────────────────────────────
+ private System.Windows.Forms.MenuStrip menuStrip1;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemTurnOffControlOnExit;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemForbidUnsafeSettings;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemMinimizeToTrayOnClose;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemAutoRefreshStats;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemCheckForUpdates;
+
+ private System.Windows.Forms.TabControl mainTabControl;
+ private System.Windows.Forms.TabPage tabPageManual;
+ private System.Windows.Forms.TabPage tabPageFanCurves;
+
+ // Manual tab
private System.Windows.Forms.TrackBar trackBarFanSpeed;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label labelValue;
@@ -249,13 +412,22 @@ private void InitializeComponent()
private System.Windows.Forms.Label labelCPUTemp;
private System.Windows.Forms.Button buttonRefreshCPUTemp;
private System.Windows.Forms.Label label4;
- private System.Windows.Forms.MenuStrip menuStrip1;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemTurnOffControlOnExit;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemForbidUnsafeSettings;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemCheckForUpdates;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemMinimizeToTrayOnClose;
- private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemAutoRefreshStats;
+
+ // Fan Curves tab
+ private System.Windows.Forms.FlowLayoutPanel panelCurveToolbar;
+ private System.Windows.Forms.Label labelProfile;
+ private System.Windows.Forms.ComboBox profileComboBox;
+ private System.Windows.Forms.Button btnSaveProfile;
+ private System.Windows.Forms.Button btnNewProfile;
+ private System.Windows.Forms.Button btnDeleteProfile;
+ private System.Windows.Forms.CheckBox checkBoxClampToGrid;
+ private FanCurvePanel fanCurvePanelCpu;
+ private FanCurvePanel fanCurvePanelGpu;
+ private System.Windows.Forms.Panel panelCurveBottom;
+ private System.Windows.Forms.Button btnStartCurve;
+ private System.Windows.Forms.Button btnStopCurve;
+ private System.Windows.Forms.Label lblCurveStatus;
+ private System.Windows.Forms.Label lblLiveCpuTemp;
+ private System.Windows.Forms.Label lblLiveRpm;
}
}
-
diff --git a/AsusFanControlGUI/Form1.cs b/AsusFanControlGUI/Form1.cs
index 1678305..1f26cab 100644
--- a/AsusFanControlGUI/Form1.cs
+++ b/AsusFanControlGUI/Form1.cs
@@ -1,73 +1,130 @@
-using AsusFanControl;
using System;
+using System.Collections.Generic;
+using System.Drawing;
using System.Windows.Forms;
+using AsusFanControl;
+using AsusFanControlGUI.Controls;
+using AsusFanControlGUI.Forms;
+using AsusFanControlGUI.Models;
+using AsusFanControlGUI.Services;
namespace AsusFanControlGUI
{
public partial class Form1 : Form
{
+ // ── Existing Manual-tab state ────────────────────────────────────────────
AsusControl asusControl = new AsusControl();
int fanSpeed = 0;
Timer timer;
NotifyIcon trayIcon;
+ // ── Fan Curves tab state ─────────────────────────────────────────────────
+ private ProfileManager _profileManager = new ProfileManager();
+ private FanCurveDaemon _daemon;
+ private FanCurveProfile _selectedProfile;
+ private bool _fanCurvesTabShownOnce = false;
+
+ // ── Constructor ──────────────────────────────────────────────────────────
public Form1()
{
InitializeComponent();
- AppDomain.CurrentDomain.ProcessExit += new EventHandler(OnProcessExit);
+ AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
- toolStripMenuItemTurnOffControlOnExit.Checked = Properties.Settings.Default.turnOffControlOnExit;
- toolStripMenuItemForbidUnsafeSettings.Checked = Properties.Settings.Default.forbidUnsafeSettings;
+ // Restore settings → Manual tab
+ toolStripMenuItemTurnOffControlOnExit.Checked = Properties.Settings.Default.turnOffControlOnExit;
+ toolStripMenuItemForbidUnsafeSettings.Checked = Properties.Settings.Default.forbidUnsafeSettings;
toolStripMenuItemMinimizeToTrayOnClose.Checked = Properties.Settings.Default.minimizeToTrayOnClose;
- toolStripMenuItemAutoRefreshStats.Checked = Properties.Settings.Default.autoRefreshStats;
+ toolStripMenuItemAutoRefreshStats.Checked = Properties.Settings.Default.autoRefreshStats;
trackBarFanSpeed.Value = Properties.Settings.Default.fanSpeed;
- }
- private void OnProcessExit(object sender, EventArgs e)
- {
- if (Properties.Settings.Default.turnOffControlOnExit)
- asusControl.SetFanSpeeds(0);
+ // Dark theme across the whole form
+ ApplyDarkTheme();
}
+ // ── Form load ────────────────────────────────────────────────────────────
private void Form1_Load(object sender, EventArgs e)
{
timerRefreshStats();
+ LoadProfiles();
+ }
+
+ private void ApplyDarkTheme()
+ {
+ // Form itself
+ BackColor = DarkTheme.Background;
+ ForeColor = DarkTheme.TextPrimary;
+
+ // Menu strip
+ menuStrip1.BackColor = Color.FromArgb(18, 18, 36);
+ menuStrip1.ForeColor = DarkTheme.TextPrimary;
+ foreach (ToolStripItem item in menuStrip1.Items)
+ {
+ item.BackColor = Color.FromArgb(18, 18, 36);
+ item.ForeColor = DarkTheme.TextPrimary;
+ }
+
+ // Tab control
+ mainTabControl.BackColor = DarkTheme.Background;
+ foreach (TabPage tp in mainTabControl.TabPages)
+ {
+ tp.BackColor = DarkTheme.Background;
+ tp.ForeColor = DarkTheme.TextPrimary;
+ }
+
+ // Manual tab controls
+ foreach (Control c in tabPageManual.Controls)
+ {
+ c.BackColor = DarkTheme.Background;
+ c.ForeColor = DarkTheme.TextPrimary;
+ if (c is Button b) DarkTheme.StyleButton(b);
+ if (c is TrackBar tb) { tb.BackColor = DarkTheme.Background; }
+ if (c is CheckBox cb) { cb.BackColor = Color.Transparent; }
+ }
+
+ // Fan Curves tab
+ DarkTheme.Apply(tabPageFanCurves);
+
+ // Style specific buttons
+ DarkTheme.StyleButton(btnSaveProfile);
+ DarkTheme.StyleButton(btnNewProfile);
+ DarkTheme.StyleButton(btnDeleteProfile);
+ DarkTheme.StyleButton(btnStartCurve);
+ DarkTheme.StyleButton(btnStopCurve);
+ DarkTheme.StyleComboBox(profileComboBox);
+ }
+
+ // ═════════════════════════════════════════════════════════════════════════
+ // MANUAL TAB — existing logic (unchanged)
+ // ═════════════════════════════════════════════════════════════════════════
+
+ private void OnProcessExit(object sender, EventArgs e)
+ {
+ if (Properties.Settings.Default.turnOffControlOnExit)
+ asusControl.SetFanSpeeds(0);
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (Properties.Settings.Default.minimizeToTrayOnClose && Visible)
{
- if(trayIcon == null)
+ if (trayIcon == null)
{
trayIcon = new NotifyIcon()
{
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();
- }),
+ 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;
-
+ if (e1.Button != MouseButtons.Left) return;
trayIcon.Visible = false;
Show();
};
}
-
trayIcon.Visible = true;
e.Cancel = true;
Hide();
@@ -76,18 +133,11 @@ private void Form1_FormClosing(object sender, FormClosingEventArgs e)
private void timerRefreshStats()
{
- if (timer != null)
- {
- timer.Stop();
- timer = null;
- }
-
- if (!Properties.Settings.Default.autoRefreshStats)
- return;
+ if (timer != null) { timer.Stop(); timer = null; }
+ if (!Properties.Settings.Default.autoRefreshStats) return;
- timer = new Timer();
- timer.Interval = 2000;
- timer.Tick += new EventHandler(TimerEventProcessor);
+ timer = new Timer { Interval = 2000 };
+ timer.Tick += TimerEventProcessor;
timer.Start();
}
@@ -119,7 +169,6 @@ private void toolStripMenuItemAutoRefreshStats_Click(object sender, EventArgs e)
{
Properties.Settings.Default.autoRefreshStats = toolStripMenuItemAutoRefreshStats.Checked;
Properties.Settings.Default.Save();
-
timerRefreshStats();
}
@@ -134,57 +183,255 @@ private void setFanSpeed()
Properties.Settings.Default.fanSpeed = value;
Properties.Settings.Default.Save();
- if (!checkBoxTurnOn.Checked)
- value = 0;
-
- if (value == 0)
- labelValue.Text = "turned off";
- else
- labelValue.Text = value.ToString();
+ if (!checkBoxTurnOn.Checked) value = 0;
- if (fanSpeed == value)
- return;
+ labelValue.Text = value == 0 ? "turned off" : value.ToString();
+ if (fanSpeed == value) return;
fanSpeed = value;
-
asusControl.SetFanSpeeds(value);
}
- private void checkBoxTurnOn_CheckedChanged(object sender, EventArgs e)
- {
- setFanSpeed();
- }
+ private void checkBoxTurnOn_CheckedChanged(object sender, EventArgs e) => setFanSpeed();
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;
+ if (trackBarFanSpeed.Value < 40) trackBarFanSpeed.Value = 40;
+ else if (trackBarFanSpeed.Value > 99) trackBarFanSpeed.Value = 99;
}
-
setFanSpeed();
}
private void trackBarFanSpeed_KeyUp(object sender, KeyEventArgs e)
{
- if (e.KeyCode != Keys.Left && e.KeyCode != Keys.Right)
- return;
-
+ if (e.KeyCode != Keys.Left && e.KeyCode != Keys.Right) return;
trackBarFanSpeed_MouseCaptureChanged(sender, e);
}
private void buttonRefreshRPM_Click(object sender, EventArgs e)
+ => labelRPM.Text = string.Join(" ", asusControl.GetFanSpeeds());
+
+ private void buttonRefreshCPUTemp_Click(object sender, EventArgs e)
+ => labelCPUTemp.Text = $"{asusControl.Thermal_Read_Cpu_Temperature()}";
+
+ // ═════════════════════════════════════════════════════════════════════════
+ // FAN CURVES TAB
+ // ═════════════════════════════════════════════════════════════════════════
+
+ private void mainTabControl_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (mainTabControl.SelectedTab == tabPageFanCurves && !_fanCurvesTabShownOnce)
+ {
+ _fanCurvesTabShownOnce = true;
+ ElevationHelper.ShowElevationWarning(lblCurveStatus);
+ }
+ }
+
+ // ── Profile loading ──────────────────────────────────────────────────────
+ private void LoadProfiles()
{
- labelRPM.Text = string.Join(" ", asusControl.GetFanSpeeds());
+ try
+ {
+ var profiles = _profileManager.LoadAll();
+ profileComboBox.Items.Clear();
+ foreach (var p in profiles)
+ profileComboBox.Items.Add(p);
+
+ if (profileComboBox.Items.Count > 0)
+ profileComboBox.SelectedIndex = 0;
+ }
+ catch (Exception ex)
+ {
+ SetStatus($"Error loading profiles: {ex.Message}", DarkTheme.ErrorRed);
+ }
}
- private void buttonRefreshCPUTemp_Click(object sender, EventArgs e)
+ private void profileComboBox_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ _selectedProfile = profileComboBox.SelectedItem as FanCurveProfile;
+ if (_selectedProfile == null) return;
+
+ bool isBuiltIn = _selectedProfile.IsBuiltIn;
+
+ fanCurvePanelCpu.Points = new List(_selectedProfile.CpuCurve);
+ fanCurvePanelCpu.IsReadOnly = isBuiltIn;
+
+ fanCurvePanelGpu.Points = new List(_selectedProfile.GpuCurve);
+ fanCurvePanelGpu.IsReadOnly = isBuiltIn;
+
+ btnSaveProfile.Enabled = !isBuiltIn;
+ btnDeleteProfile.Enabled = !isBuiltIn;
+
+ // Update daemon if running
+ if (_daemon != null && _daemon.IsRunning)
+ _daemon.ActiveProfile = _selectedProfile;
+
+ SetStatus($"Profile: {_selectedProfile.Name}{(isBuiltIn ? " (read-only)" : " (custom)")}",
+ isBuiltIn ? DarkTheme.TextSecondary : DarkTheme.AccentBlue);
+ }
+
+ // ── Clamp to Grid ────────────────────────────────────────────────────────
+ private void checkBoxClampToGrid_CheckedChanged(object sender, EventArgs e)
+ {
+ fanCurvePanelCpu.ClampToGrid = checkBoxClampToGrid.Checked;
+ fanCurvePanelGpu.ClampToGrid = checkBoxClampToGrid.Checked;
+ }
+
+ // ── Save Profile ─────────────────────────────────────────────────────────
+ private void btnSaveProfile_Click(object sender, EventArgs e)
{
- labelCPUTemp.Text = $"{asusControl.Thermal_Read_Cpu_Temperature()}";
+ if (_selectedProfile == null || _selectedProfile.IsBuiltIn) return;
+ try
+ {
+ // Sync points from panels back to profile
+ _selectedProfile.CpuCurve = new List(fanCurvePanelCpu.Points);
+ _selectedProfile.GpuCurve = new List(fanCurvePanelGpu.Points);
+ _profileManager.Save(_selectedProfile);
+ SetStatus($"Saved: {_selectedProfile.Name}", DarkTheme.SuccessGreen);
+ }
+ catch (Exception ex)
+ {
+ SetStatus($"Save error: {ex.Message}", DarkTheme.ErrorRed);
+ }
}
+ // ── New Profile ──────────────────────────────────────────────────────────
+ private void btnNewProfile_Click(object sender, EventArgs e)
+ {
+ string name = ProfileNameDialog.Show("New Profile", "Enter a name for the new profile:", this);
+ if (name == null) return;
+
+ // Check for name collision
+ foreach (var item in profileComboBox.Items)
+ {
+ if (item is FanCurveProfile p && p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ MessageBox.Show("A profile with that name already exists.", "Duplicate Name",
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+ }
+
+ // Clone from current selection or Balanced
+ var source = _selectedProfile ?? BuiltInProfiles.Balanced;
+ var newProfile = source.Clone(name);
+
+ try
+ {
+ _profileManager.Save(newProfile);
+ profileComboBox.Items.Add(newProfile);
+ profileComboBox.SelectedItem = newProfile;
+ SetStatus($"Created: {name}", DarkTheme.SuccessGreen);
+ }
+ catch (Exception ex)
+ {
+ SetStatus($"Create error: {ex.Message}", DarkTheme.ErrorRed);
+ }
+ }
+
+ // ── Delete Profile ───────────────────────────────────────────────────────
+ private void btnDeleteProfile_Click(object sender, EventArgs e)
+ {
+ if (_selectedProfile == null || _selectedProfile.IsBuiltIn) return;
+
+ var confirm = MessageBox.Show(
+ $"Delete profile '{_selectedProfile.Name}'?",
+ "Confirm Delete", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
+ if (confirm != DialogResult.Yes) return;
+
+ try
+ {
+ _profileManager.Delete(_selectedProfile);
+ int idx = profileComboBox.SelectedIndex;
+ profileComboBox.Items.Remove(_selectedProfile);
+ if (profileComboBox.Items.Count > 0)
+ profileComboBox.SelectedIndex = Math.Max(0, idx - 1);
+ SetStatus("Profile deleted.", DarkTheme.TextSecondary);
+ }
+ catch (Exception ex)
+ {
+ SetStatus($"Delete error: {ex.Message}", DarkTheme.ErrorRed);
+ }
+ }
+
+ // ── Start Curve Control ──────────────────────────────────────────────────
+ private void btnStartCurve_Click(object sender, EventArgs e)
+ {
+ if (_selectedProfile == null)
+ {
+ SetStatus("Select a profile first.", DarkTheme.WarningOrange);
+ return;
+ }
+ if (_daemon != null && _daemon.IsRunning)
+ return;
+
+ try
+ {
+ _daemon = new FanCurveDaemon(asusControl)
+ {
+ ActiveProfile = _selectedProfile,
+ LetAsusControlAtLowTemps = true
+ };
+ _daemon.StatsUpdated += OnDaemonStatsUpdated;
+ _daemon.ErrorOccurred += OnDaemonError;
+ _daemon.Start();
+
+ btnStartCurve.Enabled = false;
+ btnStopCurve.Enabled = true;
+ SetStatus($"▶ Running — {_selectedProfile.Name}", DarkTheme.SuccessGreen);
+ }
+ catch (Exception ex)
+ {
+ SetStatus($"Start error: {ex.Message}", DarkTheme.ErrorRed);
+ }
+ }
+
+ // ── Stop Curve Control ───────────────────────────────────────────────────
+ private void btnStopCurve_Click(object sender, EventArgs e)
+ {
+ _daemon?.Stop();
+ btnStartCurve.Enabled = true;
+ btnStopCurve.Enabled = false;
+ lblLiveCpuTemp.Text = "CPU: --°C";
+ lblLiveRpm.Text = "Fan: --%";
+ SetStatus("■ Stopped", DarkTheme.TextSecondary);
+ }
+
+ // ── Daemon callbacks (thread-pool → UI thread via Invoke) ────────────────
+ private void OnDaemonStatsUpdated(float cpuTempC, float fanPercent, float fanRpm)
+ {
+ if (InvokeRequired)
+ {
+ BeginInvoke(new Action(() => OnDaemonStatsUpdated(cpuTempC, fanPercent, fanRpm)));
+ return;
+ }
+ lblLiveCpuTemp.Text = $"CPU: {cpuTempC:F0}°C";
+ lblLiveRpm.Text = $"Fan: {fanPercent:F0}% ({fanRpm:F0} RPM)";
+ }
+
+ private void OnDaemonError(string message)
+ {
+ if (InvokeRequired)
+ {
+ BeginInvoke(new Action(() => OnDaemonError(message)));
+ return;
+ }
+ SetStatus($"Error: {message}", DarkTheme.ErrorRed);
+ }
+
+ // ── Helpers ──────────────────────────────────────────────────────────────
+ private void SetStatus(string text, Color color)
+ {
+ lblCurveStatus.Text = text;
+ lblCurveStatus.ForeColor = color;
+ }
+
+ protected override void OnFormClosed(FormClosedEventArgs e)
+ {
+ _daemon?.Stop();
+ base.OnFormClosed(e);
+ }
}
}
diff --git a/AsusFanControlGUI/Forms/ProfileNameDialog.cs b/AsusFanControlGUI/Forms/ProfileNameDialog.cs
new file mode 100644
index 0000000..1a2c5dc
--- /dev/null
+++ b/AsusFanControlGUI/Forms/ProfileNameDialog.cs
@@ -0,0 +1,88 @@
+using System.Windows.Forms;
+using System.Drawing;
+
+namespace AsusFanControlGUI.Forms
+{
+ /// A minimal modal dialog that asks the user to type a name string.
+ public class ProfileNameDialog : Form
+ {
+ private TextBox _textBox;
+ private Button _btnOk;
+ private Button _btnCancel;
+ private Label _promptLabel;
+
+ public string InputValue => _textBox.Text.Trim();
+
+ private ProfileNameDialog(string title, string prompt)
+ {
+ Text = title;
+ FormBorderStyle = FormBorderStyle.FixedDialog;
+ StartPosition = FormStartPosition.CenterParent;
+ ClientSize = new Size(340, 120);
+ MaximizeBox = false;
+ MinimizeBox = false;
+ BackColor = DarkTheme.Background;
+ ForeColor = DarkTheme.TextPrimary;
+
+ _promptLabel = new Label
+ {
+ Text = prompt,
+ Location = new Point(12, 14),
+ Size = new Size(316, 18),
+ ForeColor = DarkTheme.TextSecondary
+ };
+
+ _textBox = new TextBox
+ {
+ Location = new Point(12, 36),
+ Size = new Size(316, 22),
+ BackColor = DarkTheme.ButtonBackground,
+ ForeColor = DarkTheme.TextPrimary,
+ BorderStyle = BorderStyle.FixedSingle
+ };
+
+ _btnOk = new Button
+ {
+ Text = "OK",
+ DialogResult = DialogResult.OK,
+ Location = new Point(156, 72),
+ Size = new Size(80, 28)
+ };
+ DarkTheme.StyleButton(_btnOk);
+
+ _btnCancel = new Button
+ {
+ Text = "Cancel",
+ DialogResult = DialogResult.Cancel,
+ Location = new Point(248, 72),
+ Size = new Size(80, 28)
+ };
+ DarkTheme.StyleButton(_btnCancel);
+
+ AcceptButton = _btnOk;
+ CancelButton = _btnCancel;
+
+ Controls.Add(_promptLabel);
+ Controls.Add(_textBox);
+ Controls.Add(_btnOk);
+ Controls.Add(_btnCancel);
+ }
+
+ ///
+ /// Shows the dialog. Returns the trimmed name entered by the user, or null if cancelled.
+ ///
+ public static string Show(string title, string prompt, IWin32Window owner = null)
+ {
+ using (var dlg = new ProfileNameDialog(title, prompt))
+ {
+ var result = owner != null
+ ? dlg.ShowDialog(owner)
+ : dlg.ShowDialog();
+
+ if (result == DialogResult.OK && dlg.InputValue.Length > 0)
+ return dlg.InputValue;
+ return null;
+ }
+ }
+ }
+}
diff --git a/AsusFanControlGUI/Models/CurvePoint.cs b/AsusFanControlGUI/Models/CurvePoint.cs
new file mode 100644
index 0000000..560b060
--- /dev/null
+++ b/AsusFanControlGUI/Models/CurvePoint.cs
@@ -0,0 +1,22 @@
+using System.Runtime.Serialization;
+
+namespace AsusFanControlGUI.Models
+{
+ [DataContract]
+ public class CurvePoint
+ {
+ [DataMember(Name = "tempC")]
+ public float TempC { get; set; }
+
+ [DataMember(Name = "rpm")]
+ public float Rpm { get; set; } // 0 = fan stopped / OFF
+
+ public CurvePoint() { }
+
+ public CurvePoint(float tempC, float rpm)
+ {
+ TempC = tempC;
+ Rpm = rpm;
+ }
+ }
+}
diff --git a/AsusFanControlGUI/Models/FanCurveProfile.cs b/AsusFanControlGUI/Models/FanCurveProfile.cs
new file mode 100644
index 0000000..2c6a822
--- /dev/null
+++ b/AsusFanControlGUI/Models/FanCurveProfile.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace AsusFanControlGUI.Models
+{
+ [DataContract]
+ public class FanCurveProfile
+ {
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ [DataMember(Name = "isBuiltIn")]
+ public bool IsBuiltIn { get; set; }
+
+ [DataMember(Name = "cpuCurve")]
+ public List CpuCurve { get; set; }
+
+ [DataMember(Name = "gpuCurve")]
+ public List GpuCurve { get; set; }
+
+ public FanCurveProfile()
+ {
+ CpuCurve = new List();
+ GpuCurve = new List();
+ }
+
+ /// Returns a deep copy of this profile (for Custom derivation).
+ public FanCurveProfile Clone(string newName = null)
+ {
+ var clone = new FanCurveProfile
+ {
+ Name = newName ?? Name,
+ IsBuiltIn = false,
+ CpuCurve = new List(),
+ GpuCurve = new List()
+ };
+ foreach (var pt in CpuCurve) clone.CpuCurve.Add(new CurvePoint(pt.TempC, pt.Rpm));
+ foreach (var pt in GpuCurve) clone.GpuCurve.Add(new CurvePoint(pt.TempC, pt.Rpm));
+ return clone;
+ }
+ }
+}
diff --git a/AsusFanControlGUI/Program.cs b/AsusFanControlGUI/Program.cs
index 7dd3541..34bb88a 100644
--- a/AsusFanControlGUI/Program.cs
+++ b/AsusFanControlGUI/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Windows.Forms;
namespace AsusFanControlGUI
@@ -8,12 +8,44 @@ internal static class Program
///
/// The main entry point for the application.
///
+ [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+ static void RunForm()
+ {
+ System.IO.File.AppendAllText("trace.log", "Before Form1 instantiation...\n");
+ var form = new Form1();
+ System.IO.File.AppendAllText("trace.log", "Form1 instantiated. Running...\n");
+ Application.Run(form);
+ System.IO.File.AppendAllText("trace.log", "Application Run finished legally.\n");
+ }
+
[STAThread]
static void Main()
{
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- Application.Run(new Form1());
+ try
+ {
+ Environment.CurrentDirectory = System.IO.Path.GetDirectoryName(Application.ExecutablePath);
+ System.IO.File.WriteAllText("trace.log", "Starting application...\n");
+
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+
+ Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
+ Application.ThreadException += (s, e) => {
+ System.IO.File.AppendAllText("trace.log", "Thread Error: " + e.Exception.ToString() + "\n");
+ MessageBox.Show("Thread Error: " + e.Exception.ToString(), "Crash", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ };
+ AppDomain.CurrentDomain.UnhandledException += (s, e) => {
+ System.IO.File.AppendAllText("trace.log", "AppDomain Error: " + e.ExceptionObject.ToString() + "\n");
+ MessageBox.Show("AppDomain Error: " + e.ExceptionObject.ToString(), "Crash", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ };
+
+ RunForm();
+ }
+ catch (Exception ex)
+ {
+ System.IO.File.AppendAllText("trace.log", "Main Exception: " + ex.ToString() + "\n");
+ MessageBox.Show("Main Exception: " + ex.ToString(), "Crash", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
}
}
}
diff --git a/AsusFanControlGUI/Services/BuiltInProfiles.cs b/AsusFanControlGUI/Services/BuiltInProfiles.cs
new file mode 100644
index 0000000..1dd1025
--- /dev/null
+++ b/AsusFanControlGUI/Services/BuiltInProfiles.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using AsusFanControlGUI.Models;
+
+namespace AsusFanControlGUI.Services
+{
+ public static class BuiltInProfiles
+ {
+ public static readonly FanCurveProfile Turbo = new FanCurveProfile
+ {
+ Name = "Turbo",
+ IsBuiltIn = true,
+ CpuCurve = new List
+ {
+ new CurvePoint(30, 2500),
+ new CurvePoint(45, 3800),
+ new CurvePoint(50, 4300),
+ new CurvePoint(60, 4800),
+ new CurvePoint(70, 5200),
+ new CurvePoint(80, 5600),
+ new CurvePoint(90, 5800),
+ new CurvePoint(100, 6100),
+ },
+ GpuCurve = new List
+ {
+ new CurvePoint(30, 2800),
+ new CurvePoint(45, 4000),
+ new CurvePoint(50, 4600),
+ new CurvePoint(60, 4300),
+ new CurvePoint(65, 5200),
+ new CurvePoint(70, 5300),
+ new CurvePoint(80, 5800),
+ new CurvePoint(100, 6400),
+ }
+ };
+
+ public static readonly FanCurveProfile Balanced = new FanCurveProfile
+ {
+ Name = "Balanced",
+ IsBuiltIn = true,
+ CpuCurve = new List
+ {
+ new CurvePoint(30, 0),
+ new CurvePoint(60, 2200),
+ new CurvePoint(65, 2600),
+ new CurvePoint(70, 3300),
+ new CurvePoint(75, 4200),
+ new CurvePoint(80, 4300),
+ new CurvePoint(100, 4900),
+ },
+ GpuCurve = new List
+ {
+ new CurvePoint(30, 0),
+ new CurvePoint(60, 2200),
+ new CurvePoint(65, 2800),
+ new CurvePoint(70, 3800),
+ new CurvePoint(80, 4200),
+ new CurvePoint(90, 4300),
+ new CurvePoint(100, 5200),
+ }
+ };
+
+ public static readonly FanCurveProfile Silent = new FanCurveProfile
+ {
+ Name = "Silent",
+ IsBuiltIn = true,
+ CpuCurve = new List
+ {
+ new CurvePoint(30, 0),
+ new CurvePoint(60, 0),
+ new CurvePoint(70, 1800),
+ new CurvePoint(80, 2800),
+ new CurvePoint(90, 3600),
+ new CurvePoint(100, 4200),
+ },
+ GpuCurve = new List
+ {
+ new CurvePoint(30, 0),
+ new CurvePoint(60, 0),
+ new CurvePoint(70, 1800),
+ new CurvePoint(80, 2800),
+ new CurvePoint(90, 3600),
+ new CurvePoint(100, 4200),
+ }
+ };
+
+ public static List All => new List { Turbo, Balanced, Silent };
+ }
+}
diff --git a/AsusFanControlGUI/Services/FanCurveDaemon.cs b/AsusFanControlGUI/Services/FanCurveDaemon.cs
new file mode 100644
index 0000000..034804d
--- /dev/null
+++ b/AsusFanControlGUI/Services/FanCurveDaemon.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AsusFanControl;
+using AsusFanControlGUI.Models;
+
+namespace AsusFanControlGUI.Services
+{
+ ///
+ /// Background daemon that polls CPU temperature every 2 seconds, interpolates the
+ /// active fan curve, and applies the result via AsusControl.
+ ///
+ public class FanCurveDaemon
+ {
+ private readonly AsusControl _asusControl;
+ private CancellationTokenSource _cts;
+ private Task _task;
+
+ /// The profile applied by the daemon. Set before calling Start().
+ public FanCurveProfile ActiveProfile { get; set; }
+
+ ///
+ /// When true and the interpolated RPM is 0, the daemon sends percent=0 which
+ /// exits manual mode and lets ASUS firmware control the fan.
+ /// When false, the daemon physically enforces 0% duty cycle.
+ ///
+ public bool LetAsusControlAtLowTemps { get; set; } = true;
+
+ public bool IsRunning => _task != null && !_task.IsCompleted;
+
+ public const float MaxRpm = 7000f;
+
+ /// Fired on the thread pool with (cpuTempC, cpuFanPercent, cpuFanRpm).
+ public event Action StatsUpdated;
+
+ /// Fired on the thread pool when an error occurs in the loop.
+ public event Action ErrorOccurred;
+
+ public FanCurveDaemon(AsusControl asusControl)
+ {
+ _asusControl = asusControl;
+ }
+
+ public void Start()
+ {
+ if (IsRunning) return;
+ if (ActiveProfile == null)
+ throw new InvalidOperationException("ActiveProfile must be set before starting the daemon.");
+
+ _cts = new CancellationTokenSource();
+ _task = Task.Run(() => Loop(_cts.Token), _cts.Token);
+ }
+
+ public void Stop()
+ {
+ _cts?.Cancel();
+ try { _task?.Wait(3000); } catch { }
+ _task = null;
+ }
+
+ private void Loop(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ // ── Read CPU temp ────────────────────────────────────────────────
+ float cpuTemp = (float)_asusControl.Thermal_Read_Cpu_Temperature();
+
+ // ── Interpolate CPU fan target ───────────────────────────────────
+ var cpuCurve = ActiveProfile?.CpuCurve;
+ if (cpuCurve != null && cpuCurve.Count > 0)
+ {
+ float targetRpm = InterpolateRpm(cpuCurve, cpuTemp);
+ int percent = (int)Math.Round(targetRpm / MaxRpm * 100f);
+ percent = Math.Max(0, Math.Min(100, percent));
+
+ if (targetRpm == 0 && LetAsusControlAtLowTemps)
+ _asusControl.SetFanSpeeds(0); // 0 → exits test mode
+ else
+ _asusControl.SetFanSpeeds(percent);
+
+ // ── Read back actual RPM (fan 0) ─────────────────────────────
+ float actualRpm = 0;
+ try
+ {
+ var speeds = _asusControl.GetFanSpeeds();
+ if (speeds != null && speeds.Count > 0)
+ actualRpm = speeds[0];
+ }
+ catch { }
+
+ StatsUpdated?.Invoke(cpuTemp, percent, actualRpm);
+ }
+
+ // ── GPU fan (only if 2+ fans detected) ──────────────────────────
+ try
+ {
+ int fanCount = _asusControl.HealthyTable_FanCounts();
+ if (fanCount > 1)
+ {
+ var gpuCurve = ActiveProfile?.GpuCurve;
+ if (gpuCurve != null && gpuCurve.Count > 0)
+ {
+ // GPU temp is not directly readable via AsusWinIO64 in this
+ // rev of the DLL, so we skip GPU-specific interpolation and
+ // mirror CPU percent to GPU fan as a safe approximation.
+ // If GPU temp becomes available, plug it in here.
+ float gpuTargetRpm = InterpolateRpm(gpuCurve, cpuTemp);
+ int gpuPercent = (int)Math.Round(gpuTargetRpm / MaxRpm * 100f);
+ gpuPercent = Math.Max(0, Math.Min(100, gpuPercent));
+ // Set fan index 1 specifically
+ if (gpuTargetRpm == 0 && LetAsusControlAtLowTemps)
+ {
+ _asusControl.SetFanSpeed(0, 1);
+ }
+ else
+ {
+ _asusControl.SetFanSpeed(gpuPercent, 1);
+ }
+ }
+ }
+ }
+ catch { /* GPU fan optional — never crash the loop */ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(ex.Message);
+ }
+
+ // Wait 2 seconds or until cancelled
+ try { Task.Delay(2000, ct).Wait(); }
+ catch (AggregateException ae) when (ae.InnerException is TaskCanceledException) { break; }
+ }
+ }
+
+ /// Linear interpolation across sorted CurvePoint list.
+ public static float InterpolateRpm(List curve, float tempC)
+ {
+ if (curve == null || curve.Count == 0) return 0;
+ if (tempC <= curve[0].TempC) return curve[0].Rpm;
+ if (tempC >= curve[curve.Count - 1].TempC) return curve[curve.Count - 1].Rpm;
+
+ for (int i = 0; i < curve.Count - 1; i++)
+ {
+ var p1 = curve[i];
+ var p2 = curve[i + 1];
+ if (tempC >= p1.TempC && tempC <= p2.TempC)
+ {
+ float t = (tempC - p1.TempC) / (p2.TempC - p1.TempC);
+ return p1.Rpm + (p2.Rpm - p1.Rpm) * t;
+ }
+ }
+
+ return curve[curve.Count - 1].Rpm;
+ }
+ }
+}
diff --git a/AsusFanControlGUI/Services/ProfileManager.cs b/AsusFanControlGUI/Services/ProfileManager.cs
new file mode 100644
index 0000000..bfc05fc
--- /dev/null
+++ b/AsusFanControlGUI/Services/ProfileManager.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using AsusFanControlGUI.Models;
+
+namespace AsusFanControlGUI.Services
+{
+ public class ProfileManager
+ {
+ public static readonly string ProfilesDir = Path.Combine(
+ System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
+ "AsusFanControl", "profiles");
+
+ /// Returns built-in profiles plus all saved user profiles.
+ public List LoadAll()
+ {
+ var profiles = BuiltInProfiles.All;
+
+ if (!Directory.Exists(ProfilesDir))
+ Directory.CreateDirectory(ProfilesDir);
+
+ foreach (var file in Directory.GetFiles(ProfilesDir, "*.json"))
+ {
+ try
+ {
+ var profile = LoadFromFile(file);
+ if (profile != null)
+ {
+ profile.IsBuiltIn = false;
+ profiles.Add(profile);
+ }
+ }
+ catch { /* skip corrupt files */ }
+ }
+
+ return profiles;
+ }
+
+ /// Serializes a user-created profile to JSON and saves it.
+ public void Save(FanCurveProfile profile)
+ {
+ if (profile.IsBuiltIn)
+ throw new System.InvalidOperationException("Cannot save a built-in profile.");
+
+ if (!Directory.Exists(ProfilesDir))
+ Directory.CreateDirectory(ProfilesDir);
+
+ var path = Path.Combine(ProfilesDir, SanitizeFileName(profile.Name) + ".json");
+ var json = Serialize(profile);
+ File.WriteAllText(path, json, Encoding.UTF8);
+ }
+
+ /// Deletes a user-created profile file.
+ public void Delete(FanCurveProfile profile)
+ {
+ if (profile.IsBuiltIn)
+ throw new System.InvalidOperationException("Cannot delete a built-in profile.");
+
+ var path = Path.Combine(ProfilesDir, SanitizeFileName(profile.Name) + ".json");
+ if (File.Exists(path))
+ File.Delete(path);
+ }
+
+ // ── Serialization helpers using DataContractJsonSerializer ──────────────
+
+ private static string Serialize(FanCurveProfile profile)
+ {
+ var serializer = new DataContractJsonSerializer(typeof(FanCurveProfile));
+ using (var ms = new MemoryStream())
+ {
+ using (var writer = JsonReaderWriterFactory.CreateJsonWriter(ms, Encoding.UTF8, true, true, " "))
+ {
+ serializer.WriteObject(writer, profile);
+ writer.Flush();
+ }
+ return Encoding.UTF8.GetString(ms.ToArray());
+ }
+ }
+
+ private static FanCurveProfile LoadFromFile(string path)
+ {
+ var serializer = new DataContractJsonSerializer(typeof(FanCurveProfile));
+ using (var fs = File.OpenRead(path))
+ {
+ return (FanCurveProfile)serializer.ReadObject(fs);
+ }
+ }
+
+ private static string SanitizeFileName(string name)
+ {
+ foreach (var c in Path.GetInvalidFileNameChars())
+ name = name.Replace(c, '_');
+ return name;
+ }
+ }
+}
diff --git a/README.md b/README.md
index c132c78..16430cf 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,18 @@ GUI: `AsusFanControlGUI.exe`

+### 🚀 NEW: Automatic Fan Curve Control
+
+You can now set intelligent, dynamic temperature-based fan curves exactly like you would in a top-tier BIOS!
+
+
+
+Features:
+- **Interactive Graphing Editor**: Completely custom-built GUI with draggable nodes.
+- **Background Daemon**: A multithreaded daemon safely manages standard background interpolation.
+- **Custom Profiles**: Create, save, edit, and delete multiple custom curves.
+- **Smart Hysteresis & Safety Defaults**: The fans will gracefully handle spikes and automatically kick up to 100% outside safe operating ranges.
+- **Dark Theme Interface**: A totally remodeled UI following sleek, modern visual guidelines.
### 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.
diff --git a/fan-curve-tab-gui.png b/fan-curve-tab-gui.png
new file mode 100644
index 0000000..e99a6c8
Binary files /dev/null and b/fan-curve-tab-gui.png differ
diff --git a/ui-ss/fan-curve-tab-gui.png b/ui-ss/fan-curve-tab-gui.png
new file mode 100644
index 0000000..e99a6c8
Binary files /dev/null and b/ui-ss/fan-curve-tab-gui.png differ