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` ![AsusFanControlGUI](https://github.com/Karmel0x/AsusFanControl/assets/25367564/fe197ad0-7079-4d51-ae78-177cb6369e96) +### 🚀 NEW: Automatic Fan Curve Control + +You can now set intelligent, dynamic temperature-based fan curves exactly like you would in a top-tier BIOS! + +![Fan Curves UI](fan-curve-tab-gui.png) + +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