param() Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # ===================================================================== # USER SETTINGS - change these values yourself # ===================================================================== # CPU load threshold (%) that triggers 100% fans when CPU Auto is ON. $script:CpuAutoEnterThresholdDefault = 80 # CPU load threshold (%) that allows the release countdown to start. # Once this value is reached, the hold countdown starts and keeps running. # The countdown is cancelled only if CPU load rises back to the ENTER threshold. $script:CpuAutoExitThresholdDefault = 5 # Default hold time, in seconds, used when the app starts. $script:CpuAutoReleaseHoldDefault = 17 # Dropdown options for hold time, in seconds. $script:CpuAutoReleaseHoldOptions = @(0, 5, 10, 13, 15, 17, 20, 25, 30, 35, 45, 60) # Dropdown options for CPU auto ENTER threshold (%). $script:CpuAutoEnterThresholdOptions = @(10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100) $script:CpuAutoExitThresholdOptions = @(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20) # Known practical maximum fan RPM used only for percentage displays. $script:KnownMaxFanRpm = 4830 # ===================================================================== # ===================================================================== # THEME # ===================================================================== $script:ColorAppBg = [System.Drawing.Color]::FromArgb(18, 18, 20) $script:ColorCardBg = [System.Drawing.Color]::FromArgb(28, 30, 34) $script:ColorCardBorder = [System.Drawing.Color]::FromArgb(52, 56, 62) $script:ColorTextPrimary = [System.Drawing.Color]::FromArgb(240, 243, 247) $script:ColorTextMuted = [System.Drawing.Color]::FromArgb(160, 166, 175) $script:ColorAccent = [System.Drawing.Color]::FromArgb(91, 157, 255) $script:ColorSuccess = [System.Drawing.Color]::FromArgb(66, 184, 131) $script:ColorWarning = [System.Drawing.Color]::FromArgb(255, 179, 71) $script:ColorDanger = [System.Drawing.Color]::FromArgb(224, 87, 87) $script:ColorButtonDark = [System.Drawing.Color]::FromArgb(42, 45, 51) $script:ColorButtonHover = [System.Drawing.Color]::FromArgb(60, 64, 72) $script:ColorInputBg = [System.Drawing.Color]::FromArgb(34, 36, 40) $script:FontUi = New-Object System.Drawing.Font('Segoe UI', 9.75, [System.Drawing.FontStyle]::Regular) $script:FontUiBold = New-Object System.Drawing.Font('Segoe UI Semibold', 9.75, [System.Drawing.FontStyle]::Bold) $script:FontTitle = New-Object System.Drawing.Font('Segoe UI Semibold', 13.0, [System.Drawing.FontStyle]::Bold) $script:FontCardTitle = New-Object System.Drawing.Font('Segoe UI Semibold', 10.5, [System.Drawing.FontStyle]::Bold) $script:FontValue = New-Object System.Drawing.Font('Segoe UI Semibold', 10.5, [System.Drawing.FontStyle]::Bold) # ===================================================================== function Test-IsAdmin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object Security.Principal.WindowsPrincipal($id) return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Restart-Elevated { $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = 'powershell.exe' $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$PSCommandPath`"" $psi.Verb = 'runas' $psi.UseShellExecute = $true [void][System.Diagnostics.Process]::Start($psi) } if (-not (Test-IsAdmin)) { try { Restart-Elevated } catch { [System.Windows.Forms.MessageBox]::Show( "Script must run as administrator.`r`n`r`n$($_.Exception.Message)", 'Dell G15 Fan Control', [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null } exit } $script:AppName = 'Dell G15 Fan Control Author: Lilita Bogachkova' $script:CpuFanId = 51 $script:GpuFanId = 50 $script:AwccObject = $null $script:ObservedPeakCpuRpm = 0 $script:ObservedPeakGpuRpm = 0 $script:LastCommand = 'Application started' $script:LastRefreshTime = [datetime]::MinValue $script:IsBusy = $false $script:CpuCounter = $null $script:CpuCounterName = 'Not initialized' # CPU auto is ON by default. $script:CpuAutoEnabled = $true $script:CpuAutoLatched = $false $script:CpuAutoEnterThreshold = [double]$script:CpuAutoEnterThresholdDefault $script:CpuAutoExitThreshold = [double]$script:CpuAutoExitThresholdDefault $script:CpuAutoReleaseHoldSeconds = [int]$script:CpuAutoReleaseHoldDefault $script:CpuAutoReleasePendingUntil = [datetime]::MinValue # Manual force state. CPU auto has higher priority whenever its logic becomes active. $script:ManualForceEnabled = $false function Set-DoubleBuffered { param([System.Windows.Forms.Control]$Control) try { $prop = $Control.GetType().GetProperty('DoubleBuffered', [System.Reflection.BindingFlags]'NonPublic,Instance') if ($null -ne $prop) { $prop.SetValue($Control, $true, $null) } } catch {} } function New-CardPanel { param( [string]$Title, [int]$X, [int]$Y, [int]$Width, [int]$Height ) $panel = New-Object System.Windows.Forms.Panel $panel.Location = New-Object System.Drawing.Point($X, $Y) $panel.Size = New-Object System.Drawing.Size($Width, $Height) $panel.BackColor = $script:ColorCardBg $panel.BorderStyle = 'FixedSingle' Set-DoubleBuffered -Control $panel $titleLabel = New-Object System.Windows.Forms.Label $titleLabel.Text = $Title $titleLabel.Location = New-Object System.Drawing.Point(16, 12) $titleLabel.Size = New-Object System.Drawing.Size(($Width - 32), 24) $titleLabel.Font = $script:FontCardTitle $titleLabel.ForeColor = $script:ColorTextPrimary $titleLabel.BackColor = $script:ColorCardBg $separator = New-Object System.Windows.Forms.Panel $separator.Location = New-Object System.Drawing.Point(16, 40) $separator.Size = New-Object System.Drawing.Size(($Width - 32), 1) $separator.BackColor = $script:ColorCardBorder $panel.Controls.AddRange(@($titleLabel, $separator)) return $panel } function New-InfoLabel { param([string]$Text, [int]$X, [int]$Y, [int]$Width) $lbl = New-Object System.Windows.Forms.Label $lbl.Text = $Text $lbl.Location = New-Object System.Drawing.Point($X, $Y) $lbl.Size = New-Object System.Drawing.Size($Width, 22) $lbl.Font = $script:FontUi $lbl.ForeColor = $script:ColorTextMuted $lbl.BackColor = [System.Drawing.Color]::Transparent return $lbl } function New-ValueLabel { param([string]$Text, [int]$X, [int]$Y, [int]$Width) $lbl = New-Object System.Windows.Forms.Label $lbl.Text = $Text $lbl.Location = New-Object System.Drawing.Point($X, $Y) $lbl.Size = New-Object System.Drawing.Size($Width, 22) $lbl.Font = $script:FontValue $lbl.ForeColor = $script:ColorTextPrimary $lbl.BackColor = [System.Drawing.Color]::Transparent return $lbl } function New-ModernButton { param( [string]$Text, [int]$X, [int]$Y, [int]$Width, [int]$Height, [System.Drawing.Color]$BackColor, [System.Drawing.Color]$ForeColor ) $btn = New-Object System.Windows.Forms.Button $btn.Text = $Text $btn.Location = New-Object System.Drawing.Point($X, $Y) $btn.Size = New-Object System.Drawing.Size($Width, $Height) $btn.Font = $script:FontUiBold $btn.FlatStyle = 'Flat' $btn.FlatAppearance.BorderSize = 0 $btn.FlatAppearance.MouseOverBackColor = $script:ColorButtonHover $btn.FlatAppearance.MouseDownBackColor = $script:ColorButtonHover $btn.BackColor = $BackColor $btn.ForeColor = $ForeColor $btn.TabStop = $false return $btn } function New-ReadoutBox { param([string]$Text, [int]$X, [int]$Y, [int]$Width) $lbl = New-Object System.Windows.Forms.Label $lbl.Text = $Text $lbl.Location = New-Object System.Drawing.Point($X, $Y) $lbl.Size = New-Object System.Drawing.Size($Width, 24) $lbl.Font = $script:FontUiBold $lbl.ForeColor = $script:ColorTextPrimary $lbl.BackColor = $script:ColorInputBg $lbl.BorderStyle = 'FixedSingle' $lbl.TextAlign = 'MiddleCenter' return $lbl } function Set-LabelText { param([System.Windows.Forms.Label]$Label, [string]$Text) if ($Label.Text -ne $Text) { $Label.Text = $Text } } function Initialize-AutoUiState { $script:CpuAutoEnabled = $true $script:CpuAutoLatched = $false $script:ManualForceEnabled = $false Clear-PendingRelease Update-AutoButtonText Update-StateBadge -StateText (Get-AutoStateText) } function Test-IsPendingRelease { return ($script:CpuAutoReleasePendingUntil -gt [datetime]::MinValue) } function Clear-PendingRelease { $script:CpuAutoReleasePendingUntil = [datetime]::MinValue } function Get-AwccObject { if ($null -ne $script:AwccObject) { return $script:AwccObject } $obj = Get-WmiObject -Namespace 'root\WMI' -Class 'AWCCWmiMethodFunction' | Select-Object -First 1 if ($null -eq $obj) { throw "AWCCWmiMethodFunction object was not found in root\\WMI." } $script:AwccObject = $obj return $script:AwccObject } function Invoke-AwccMethod { param( [Parameter(Mandatory)][string]$MethodName, [Parameter(Mandatory)][uint32]$Arg2 ) $obj = Get-AwccObject if (-not ($obj | Get-Member -Name $MethodName -MemberType Method)) { throw "AWCC method '$MethodName' is not available." } $result = $obj.$MethodName($Arg2) if ($null -eq $result) { throw "AWCC method '$MethodName' returned null." } if ($result -is [array]) { if ($result.Count -lt 1) { throw "AWCC method '$MethodName' returned an empty array." } return [uint32]$result[0] } if ($result.PSObject.Properties.Match('argr').Count -gt 0) { return [uint32]$result.argr } return [uint32]$result } function Set-ThermalModeByCode { param([Parameter(Mandatory)][int]$ModeCode) $arg = (([uint32]($ModeCode -band 0xFF)) -shl 8) -bor 1 $result = Invoke-AwccMethod -MethodName 'Thermal_Control' -Arg2 ([uint32]$arg) return ($result -eq 0) } function Set-BalancedMode { if (Set-ThermalModeByCode -ModeCode 160) { return $true } return (Set-ThermalModeByCode -ModeCode 151) } function Set-FullSpeedMode { return (Set-ThermalModeByCode -ModeCode 153) } function Set-CustomMode { return (Set-ThermalModeByCode -ModeCode 0) } function Set-FanPercent { param( [Parameter(Mandatory)][int]$FanId, [Parameter(Mandatory)][int]$Percent ) $p = [Math]::Max(0, [Math]::Min(100, $Percent)) $arg = (([uint32]($p -band 0xFF)) -shl 16) -bor (([uint32]($FanId -band 0xFF)) -shl 8) -bor 2 $result = Invoke-AwccMethod -MethodName 'Thermal_Control' -Arg2 ([uint32]$arg) return ($result -eq 0) } function Get-FanRpm { param([Parameter(Mandatory)][int]$FanId) $arg = (([uint32]($FanId -band 0xFF)) -shl 8) -bor 5 $raw = Invoke-AwccMethod -MethodName 'Thermal_Information' -Arg2 ([uint32]$arg) if ($raw -eq 0xFFFFFFFF) { return $null } if ($raw -gt 20000) { return $null } return [int]$raw } function Initialize-CpuCounter { if ($null -ne $script:CpuCounter) { return } try { $script:CpuCounter = New-Object System.Diagnostics.PerformanceCounter('Processor Information', '% Processor Utility', '_Total', $true) $script:CpuCounterName = 'Processor Information / % Processor Utility' } catch { $script:CpuCounter = New-Object System.Diagnostics.PerformanceCounter('Processor', '% Processor Time', '_Total', $true) $script:CpuCounterName = 'Processor / % Processor Time' } try { $null = $script:CpuCounter.NextValue() } catch {} } function Get-CpuLoadValue { Initialize-CpuCounter try { $value = [double]$script:CpuCounter.NextValue() } catch { return 0.0 } if ($value -lt 0) { $value = 0 } if ($value -gt 100) { $value = 100 } return [Math]::Round($value, 1) } function Update-Peaks { param($CpuRpm, $GpuRpm) if ($null -ne $CpuRpm -and [int]$CpuRpm -gt $script:ObservedPeakCpuRpm) { $script:ObservedPeakCpuRpm = [int]$CpuRpm } if ($null -ne $GpuRpm -and [int]$GpuRpm -gt $script:ObservedPeakGpuRpm) { $script:ObservedPeakGpuRpm = [int]$GpuRpm } } function Get-PercentOfReference { param($Current, [int]$Reference) if ($Reference -le 0 -or $null -eq $Current) { return '-' } $pct = [Math]::Round(([double]$Current / [double]$Reference) * 100.0, 1) return ('{0:N1} %' -f $pct) } function Format-RpmVsKnownMax { param($RpmValue) if ($null -eq $RpmValue -or [int]$script:KnownMaxFanRpm -le 0) { return '-' } $pct = [Math]::Round(([double]$RpmValue / [double]$script:KnownMaxFanRpm) * 100.0, 1) return ('{0} / {1} ({2:N1} %)' -f [int]$RpmValue, [int]$script:KnownMaxFanRpm, $pct) } function Update-AutoButtonText { if ($script:ManualForceEnabled) { $btnForce100.Text = 'Force 100%: ON' $btnForce100.BackColor = $script:ColorDanger $btnReleaseAuto.Text = 'Return to Auto: OFF' $btnReleaseAuto.BackColor = $script:ColorButtonDark $btnCpuAuto.Text = 'CPU Load Auto: OFF' $btnCpuAuto.BackColor = $script:ColorButtonDark return } if ($script:CpuAutoEnabled) { $btnForce100.Text = 'Force 100%: OFF' $btnForce100.BackColor = $script:ColorButtonDark $btnReleaseAuto.Text = 'Return to Auto: OFF' $btnReleaseAuto.BackColor = $script:ColorButtonDark $btnCpuAuto.Text = 'CPU Load Auto: ON' $btnCpuAuto.BackColor = $script:ColorAccent return } $btnForce100.Text = 'Force 100%: OFF' $btnForce100.BackColor = $script:ColorButtonDark $btnReleaseAuto.Text = 'Return to Auto: ON' $btnReleaseAuto.BackColor = $script:ColorSuccess $btnCpuAuto.Text = 'CPU Load Auto: OFF' $btnCpuAuto.BackColor = $script:ColorButtonDark } function Update-StateBadge { param([string]$StateText) Set-LabelText -Label $lblAutoStateValue -Text $StateText if ($StateText -like 'Hold *' -or $StateText -like 'Latched*' -or $StateText -like '*manual 100%') { $lblAutoStateValue.ForeColor = $script:ColorWarning } elseif ($StateText -eq 'Armed' -or $StateText -eq 'Dell Auto') { $lblAutoStateValue.ForeColor = $script:ColorSuccess } elseif ($StateText -eq 'Disabled') { $lblAutoStateValue.ForeColor = $script:ColorTextMuted } else { $lblAutoStateValue.ForeColor = $script:ColorTextPrimary } } function Update-StatusBar { param([string]$Text) Set-LabelText -Label $lblStatus -Text $Text if ($Text -like '*error*' -or $Text -like '*failed*') { $statusPanel.BackColor = [System.Drawing.Color]::FromArgb(64, 29, 29) $lblStatus.ForeColor = $script:ColorDanger } elseif ($Text -like '*100%*' -or $Text -like '*latched*' -or $Text -like '*Holding*') { $statusPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 45, 28) $lblStatus.ForeColor = $script:ColorWarning } else { $statusPanel.BackColor = [System.Drawing.Color]::FromArgb(26, 28, 32) $lblStatus.ForeColor = $script:ColorTextPrimary } } function Update-MetaDisplay { if ($null -ne $lblLastCommandValue) { Set-LabelText -Label $lblLastCommandValue -Text ([string]$script:LastCommand) } if ($null -ne $lblLastRefreshValue) { if ($script:LastRefreshTime -is [datetime] -and $script:LastRefreshTime -gt [datetime]::MinValue) { Set-LabelText -Label $lblLastRefreshValue -Text ($script:LastRefreshTime.ToString('HH:mm:ss')) } else { Set-LabelText -Label $lblLastRefreshValue -Text '-' } } } function Get-AutoStateText { if (Test-IsPendingRelease) { $remaining = [int][Math]::Ceiling(($script:CpuAutoReleasePendingUntil - (Get-Date)).TotalSeconds) if ($remaining -lt 0) { $remaining = 0 } return ('Hold {0}s' -f $remaining) } if ($script:CpuAutoLatched) { return 'Latched 100%' } if ($script:ManualForceEnabled) { return 'Manual 100%' } if ($script:CpuAutoEnabled) { return 'Armed' } return 'Dell Auto' } function Send-MaxFansCore { $ok = $false try { $ok = Set-FullSpeedMode } catch { $ok = $false } if (-not $ok) { [void](Set-CustomMode) $ok1 = Set-FanPercent -FanId $script:CpuFanId -Percent 100 $ok2 = Set-FanPercent -FanId $script:GpuFanId -Percent 100 $ok = ($ok1 -and $ok2) } if (-not $ok) { throw 'Unable to force maximum fan speed.' } } function Send-AutoCore { if (-not (Set-BalancedMode)) { throw 'Unable to return fans to automatic mode.' } } function Enable-MaxFans { Send-MaxFansCore # Manual mode must disable CPU auto logic and cancel any active hold countdown. $script:CpuAutoEnabled = $false Clear-PendingRelease $script:CpuAutoLatched = $false $script:ManualForceEnabled = $true $script:LastCommand = 'Force 100%' Update-StatusBar -Text 'Manual 100% fan mode enabled. CPU load auto disabled.' Update-MetaDisplay Update-AutoButtonText Update-StateBadge -StateText (Get-AutoStateText) } function Release-ToAuto { Send-AutoCore # Returning to automatic mode must disable CPU auto logic and cancel any active hold countdown. $script:CpuAutoEnabled = $false Clear-PendingRelease $script:CpuAutoLatched = $false $script:ManualForceEnabled = $false $script:LastCommand = 'Return to Auto' Update-StatusBar -Text 'Returned fan control to Dell automatic mode. CPU load auto disabled.' Update-MetaDisplay Update-AutoButtonText Update-StateBadge -StateText (Get-AutoStateText) } function Toggle-CpuAutoMode { $script:CpuAutoEnabled = -not $script:CpuAutoEnabled if (-not $script:CpuAutoEnabled) { Clear-PendingRelease $script:CpuAutoLatched = $false $script:ManualForceEnabled = $false Send-AutoCore $script:LastCommand = 'CPU Auto OFF' Update-StatusBar -Text 'CPU load auto control disabled. Returned to Dell automatic mode.' } else { Clear-PendingRelease $script:CpuAutoLatched = $false $script:ManualForceEnabled = $false Send-AutoCore $script:LastCommand = 'CPU Auto ON' Update-StatusBar -Text ('CPU load auto control enabled. Enter: {0}% | Exit: {1}% | Hold: {2}s.' -f [int]$script:CpuAutoEnterThreshold, [int]$script:CpuAutoExitThreshold, $script:CpuAutoReleaseHoldSeconds) } Update-AutoButtonText Update-StateBadge -StateText (Get-AutoStateText) Update-MetaDisplay Refresh-RpmStatus } function Evaluate-CpuAuto { param([double]$CpuLoad) if (-not $script:CpuAutoEnabled) { return } if (-not $script:CpuAutoLatched) { if ($CpuLoad -ge $script:CpuAutoEnterThreshold) { Send-MaxFansCore $script:CpuAutoLatched = $true $script:ManualForceEnabled = $false Clear-PendingRelease $script:LastCommand = 'CPU Auto -> Force 100%' Update-StatusBar -Text 'CPU load auto control locked fans at 100%.' Update-MetaDisplay } return } if (Test-IsPendingRelease) { if ($CpuLoad -ge $script:CpuAutoEnterThreshold) { Clear-PendingRelease $script:LastCommand = 'CPU Auto -> Hold cancelled' Update-StatusBar -Text 'Hold countdown cancelled; staying at 100% fan speed.' Update-MetaDisplay return } if ((Get-Date) -ge $script:CpuAutoReleasePendingUntil) { Send-AutoCore $script:CpuAutoLatched = $false $script:ManualForceEnabled = $false Clear-PendingRelease $script:LastCommand = 'CPU Auto -> Release to Auto' Update-StatusBar -Text 'Hold elapsed; returned fan control to automatic mode.' Update-MetaDisplay } return } if ($CpuLoad -le $script:CpuAutoExitThreshold) { $script:CpuAutoReleasePendingUntil = (Get-Date).AddSeconds([int]$script:CpuAutoReleaseHoldSeconds) $script:LastCommand = 'CPU Auto -> Hold started' Update-StatusBar -Text ('Exit threshold reached. Holding 100% fan speed for {0}s.' -f $script:CpuAutoReleaseHoldSeconds) Update-MetaDisplay } } function Refresh-RpmStatus { if ($script:IsBusy) { return } $script:IsBusy = $true try { $script:LastRefreshTime = Get-Date $cpuLoad = Get-CpuLoadValue Evaluate-CpuAuto -CpuLoad $cpuLoad $cpu = Get-FanRpm -FanId $script:CpuFanId $gpu = Get-FanRpm -FanId $script:GpuFanId Update-Peaks -CpuRpm $cpu -GpuRpm $gpu Set-LabelText -Label $lblCpuRpmValue -Text ($(if ($null -ne $cpu) { [string][int]$cpu } else { '-' })) Set-LabelText -Label $lblGpuRpmValue -Text ($(if ($null -ne $gpu) { [string][int]$gpu } else { '-' })) Set-LabelText -Label $lblCpuPctBox -Text (Get-PercentOfReference -Current $cpu -Reference $script:KnownMaxFanRpm) Set-LabelText -Label $lblGpuPctBox -Text (Get-PercentOfReference -Current $gpu -Reference $script:KnownMaxFanRpm) Set-LabelText -Label $lblCpuPeakValue -Text (Format-RpmVsKnownMax -RpmValue $script:ObservedPeakCpuRpm) Set-LabelText -Label $lblGpuPeakValue -Text (Format-RpmVsKnownMax -RpmValue $script:ObservedPeakGpuRpm) Set-LabelText -Label $lblCpuLoadValue -Text ('{0:N1} %' -f $cpuLoad) Update-StateBadge -StateText (Get-AutoStateText) Update-MetaDisplay } catch { Update-StatusBar -Text ("RPM refresh error: " + $_.Exception.Message) } finally { $script:IsBusy = $false } } function Reset-Peaks { $script:ObservedPeakCpuRpm = 0 $script:ObservedPeakGpuRpm = 0 $script:LastCommand = 'Reset peaks' Update-StatusBar -Text 'Observed peak RPM counters reset.' Update-MetaDisplay Refresh-RpmStatus } function Restore-OriginalFanControlOnExit { try { $script:CpuAutoEnabled = $false $script:CpuAutoLatched = $false $script:ManualForceEnabled = $false Clear-PendingRelease Send-AutoCore $script:LastCommand = 'Exit -> Release to Auto' } catch {} } # --------------------------------------------------------------------- # UI # --------------------------------------------------------------------- $form = New-Object System.Windows.Forms.Form $form.SuspendLayout() $form.Text = $script:AppName $form.StartPosition = 'CenterScreen' $form.Size = New-Object System.Drawing.Size(860, 638) $form.MinimumSize = New-Object System.Drawing.Size(860, 640) $form.FormBorderStyle = 'FixedDialog' $form.MaximizeBox = $false $form.BackColor = $script:ColorAppBg $form.ForeColor = $script:ColorTextPrimary $form.Font = $script:FontUi Set-DoubleBuffered -Control $form $lblHeaderTitle = New-Object System.Windows.Forms.Label $lblHeaderTitle.Text = 'Dell G15 Fan Control' $lblHeaderTitle.Location = New-Object System.Drawing.Point(20, 16) $lblHeaderTitle.Size = New-Object System.Drawing.Size(360, 28) $lblHeaderTitle.Font = $script:FontTitle $lblHeaderTitle.ForeColor = $script:ColorTextPrimary $lblHeaderTitle.BackColor = $script:ColorAppBg $lblHeaderSub = New-Object System.Windows.Forms.Label $lblHeaderSub.Text = 'Manual fan override, configurable CPU-load auto control, and lightweight live telemetry.' $lblHeaderSub.Location = New-Object System.Drawing.Point(22, 46) $lblHeaderSub.Size = New-Object System.Drawing.Size(560, 22) $lblHeaderSub.Font = $script:FontUi $lblHeaderSub.ForeColor = $script:ColorTextMuted $lblHeaderSub.BackColor = $script:ColorAppBg $actionPanel = New-CardPanel -Title 'Control Actions' -X 20 -Y 80 -Width 810 -Height 92 $btnForce100 = New-ModernButton -Text 'Force 100%: OFF' -X 18 -Y 50 -Width 180 -Height 30 -BackColor $script:ColorButtonDark -ForeColor $script:ColorTextPrimary $btnReleaseAuto = New-ModernButton -Text 'Return to Auto: OFF' -X 210 -Y 50 -Width 180 -Height 30 -BackColor $script:ColorButtonDark -ForeColor $script:ColorTextPrimary $btnCpuAuto = New-ModernButton -Text 'CPU Load Auto: ON' -X 402 -Y 50 -Width 190 -Height 30 -BackColor $script:ColorAccent -ForeColor $script:ColorTextPrimary $btnResetPeaks = New-ModernButton -Text 'Reset peak RPM' -X 604 -Y 50 -Width 188 -Height 30 -BackColor $script:ColorButtonDark -ForeColor $script:ColorTextPrimary $actionPanel.Controls.AddRange(@($btnForce100, $btnReleaseAuto, $btnCpuAuto, $btnResetPeaks)) $telemetryPanel = New-CardPanel -Title 'Live Telemetry' -X 20 -Y 188 -Width 390 -Height 222 $lblCpuRpm = New-InfoLabel -Text 'CPU Fan RPM' -X 18 -Y 58 -Width 120 $lblCpuRpmValue = New-ValueLabel -Text '-' -X 170 -Y 58 -Width 95 $lblCpuPctBox = New-ReadoutBox -Text '-' -X 272 -Y 56 -Width 88 $lblGpuRpm = New-InfoLabel -Text 'GPU Fan RPM' -X 18 -Y 90 -Width 120 $lblGpuRpmValue = New-ValueLabel -Text '-' -X 170 -Y 90 -Width 95 $lblGpuPctBox = New-ReadoutBox -Text '-' -X 272 -Y 88 -Width 88 $lblCpuLoad = New-InfoLabel -Text 'CPU Load' -X 18 -Y 122 -Width 120 $lblCpuLoadValue = New-ValueLabel -Text '-' -X 170 -Y 122 -Width 190 $lblAutoState = New-InfoLabel -Text 'Auto state' -X 18 -Y 154 -Width 120 $lblAutoStateValue = New-ValueLabel -Text 'Armed' -X 170 -Y 154 -Width 190 $telemetryPanel.Controls.AddRange(@( $lblCpuRpm, $lblCpuRpmValue, $lblCpuPctBox, $lblGpuRpm, $lblGpuRpmValue, $lblGpuPctBox, $lblCpuLoad, $lblCpuLoadValue, $lblAutoState, $lblAutoStateValue )) $statsPanel = New-CardPanel -Title 'Observed Peaks & Activity' -X 440 -Y 188 -Width 390 -Height 222 $lblCpuPeak = New-InfoLabel -Text 'CPU Peak RPM' -X 18 -Y 58 -Width 120 $lblCpuPeakValue = New-ValueLabel -Text '-' -X 180 -Y 58 -Width 190 $lblGpuPeak = New-InfoLabel -Text 'GPU Peak RPM' -X 18 -Y 90 -Width 120 $lblGpuPeakValue = New-ValueLabel -Text '-' -X 180 -Y 90 -Width 190 $lblLastCommand = New-InfoLabel -Text 'Last action' -X 18 -Y 122 -Width 120 $lblLastCommandValue = New-ValueLabel -Text 'Application started' -X 180 -Y 122 -Width 210 $lblLastRefresh = New-InfoLabel -Text 'Last update' -X 18 -Y 154 -Width 120 $lblLastRefreshValue = New-ValueLabel -Text '-' -X 180 -Y 154 -Width 190 $statsPanel.Controls.AddRange(@( $lblCpuPeak, $lblCpuPeakValue, $lblGpuPeak, $lblGpuPeakValue, $lblLastCommand, $lblLastCommandValue, $lblLastRefresh, $lblLastRefreshValue )) $configPanel = New-CardPanel -Title 'Control Settings' -X 20 -Y 426 -Width 810 -Height 122 $settingsCard1 = New-Object System.Windows.Forms.Panel $settingsCard1.Location = New-Object System.Drawing.Point(20, 50) $settingsCard1.Size = New-Object System.Drawing.Size(236, 54) $settingsCard1.BackColor = $script:ColorInputBg $settingsCard1.BorderStyle = 'FixedSingle' $settingsCard2 = New-Object System.Windows.Forms.Panel $settingsCard2.Location = New-Object System.Drawing.Point(287, 50) $settingsCard2.Size = New-Object System.Drawing.Size(236, 54) $settingsCard2.BackColor = $script:ColorInputBg $settingsCard2.BorderStyle = 'FixedSingle' $settingsCard3 = New-Object System.Windows.Forms.Panel $settingsCard3.Location = New-Object System.Drawing.Point(554, 50) $settingsCard3.Size = New-Object System.Drawing.Size(236, 54) $settingsCard3.BackColor = $script:ColorInputBg $settingsCard3.BorderStyle = 'FixedSingle' $lblRefreshInterval = New-Object System.Windows.Forms.Label $lblRefreshInterval.Text = 'Refresh interval (ms)' $lblRefreshInterval.Location = New-Object System.Drawing.Point(12, 8) $lblRefreshInterval.Size = New-Object System.Drawing.Size(150, 18) $lblRefreshInterval.Font = $script:FontUi $lblRefreshInterval.ForeColor = $script:ColorTextMuted $lblRefreshInterval.BackColor = $script:ColorInputBg $numRefreshInterval = New-Object System.Windows.Forms.NumericUpDown $numRefreshInterval.Location = New-Object System.Drawing.Point(12, 26) $numRefreshInterval.Size = New-Object System.Drawing.Size(110, 24) $numRefreshInterval.Minimum = 250 $numRefreshInterval.Maximum = 5000 $numRefreshInterval.Increment = 250 $numRefreshInterval.Value = 500 $numRefreshInterval.Font = $script:FontUi $numRefreshInterval.BackColor = $script:ColorInputBg $numRefreshInterval.ForeColor = $script:ColorTextPrimary $numRefreshInterval.BorderStyle = 'FixedSingle' $lblAutoHold = New-Object System.Windows.Forms.Label $lblAutoHold.Text = 'Auto release hold (s)' $lblAutoHold.Location = New-Object System.Drawing.Point(12, 8) $lblAutoHold.Size = New-Object System.Drawing.Size(160, 18) $lblAutoHold.Font = $script:FontUi $lblAutoHold.ForeColor = $script:ColorTextMuted $lblAutoHold.BackColor = $script:ColorInputBg $comboAutoHold = New-Object System.Windows.Forms.ComboBox $comboAutoHold.Location = New-Object System.Drawing.Point(12, 26) $comboAutoHold.Size = New-Object System.Drawing.Size(110, 24) $comboAutoHold.DropDownStyle = 'DropDownList' $comboAutoHold.Font = $script:FontUi $comboAutoHold.BackColor = $script:ColorInputBg $comboAutoHold.ForeColor = $script:ColorTextPrimary $comboAutoHold.FlatStyle = 'Flat' $lblThresholdTitle = New-Object System.Windows.Forms.Label $lblThresholdTitle.Text = 'CPU auto thresholds' $lblThresholdTitle.Location = New-Object System.Drawing.Point(12, 6) $lblThresholdTitle.Size = New-Object System.Drawing.Size(170, 18) $lblThresholdTitle.Font = $script:FontUi $lblThresholdTitle.ForeColor = $script:ColorTextMuted $lblThresholdTitle.BackColor = $script:ColorInputBg $lblEnterMini = New-Object System.Windows.Forms.Label $lblEnterMini.Text = 'Enter' $lblEnterMini.Location = New-Object System.Drawing.Point(12, 30) $lblEnterMini.Size = New-Object System.Drawing.Size(36, 18) $lblEnterMini.Font = $script:FontUi $lblEnterMini.ForeColor = $script:ColorTextMuted $lblEnterMini.BackColor = $script:ColorInputBg $comboEnterThreshold = New-Object System.Windows.Forms.ComboBox $comboEnterThreshold.Location = New-Object System.Drawing.Point(50, 26) $comboEnterThreshold.Size = New-Object System.Drawing.Size(66, 24) $comboEnterThreshold.DropDownStyle = 'DropDownList' $comboEnterThreshold.Font = $script:FontUi $comboEnterThreshold.BackColor = $script:ColorInputBg $comboEnterThreshold.ForeColor = $script:ColorTextPrimary $comboEnterThreshold.FlatStyle = 'Flat' $lblExitMini = New-Object System.Windows.Forms.Label $lblExitMini.Text = 'Exit' $lblExitMini.Location = New-Object System.Drawing.Point(126, 30) $lblExitMini.Size = New-Object System.Drawing.Size(26, 18) $lblExitMini.Font = $script:FontUi $lblExitMini.ForeColor = $script:ColorTextMuted $lblExitMini.BackColor = $script:ColorInputBg $comboExitThreshold = New-Object System.Windows.Forms.ComboBox $comboExitThreshold.Location = New-Object System.Drawing.Point(156, 26) $comboExitThreshold.Size = New-Object System.Drawing.Size(66, 24) $comboExitThreshold.DropDownStyle = 'DropDownList' $comboExitThreshold.Font = $script:FontUi $comboExitThreshold.BackColor = $script:ColorInputBg $comboExitThreshold.ForeColor = $script:ColorTextPrimary $comboExitThreshold.FlatStyle = 'Flat' foreach ($sec in $script:CpuAutoReleaseHoldOptions) { [void]$comboAutoHold.Items.Add([string][int]$sec) } foreach ($thr in $script:CpuAutoEnterThresholdOptions) { [void]$comboEnterThreshold.Items.Add([string][int]$thr) } foreach ($thr in $script:CpuAutoExitThresholdOptions) { [void]$comboExitThreshold.Items.Add([string][int]$thr) } $settingsCard1.Controls.AddRange(@($lblRefreshInterval, $numRefreshInterval)) $settingsCard2.Controls.AddRange(@($lblAutoHold, $comboAutoHold)) $settingsCard3.Controls.AddRange(@($lblThresholdTitle, $lblEnterMini, $comboEnterThreshold, $lblExitMini, $comboExitThreshold)) $configPanel.Controls.AddRange(@( $settingsCard1, $settingsCard2, $settingsCard3 )) $statusPanel = New-Object System.Windows.Forms.Panel $statusPanel.Location = New-Object System.Drawing.Point(20, 562) $statusPanel.Size = New-Object System.Drawing.Size(810, 28) $statusPanel.BackColor = [System.Drawing.Color]::FromArgb(26, 28, 32) $statusPanel.BorderStyle = 'FixedSingle' $lblStatus = New-Object System.Windows.Forms.Label $lblStatus.Text = 'Ready.' $lblStatus.Location = New-Object System.Drawing.Point(12, 4) $lblStatus.Size = New-Object System.Drawing.Size(780, 20) $lblStatus.Font = $script:FontUi $lblStatus.ForeColor = $script:ColorTextPrimary $lblStatus.BackColor = $statusPanel.BackColor $statusPanel.Controls.Add($lblStatus) $form.Controls.AddRange(@( $lblHeaderTitle, $lblHeaderSub, $actionPanel, $telemetryPanel, $statsPanel, $configPanel, $statusPanel )) $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 500 $timer.add_Tick({ Refresh-RpmStatus }) $numRefreshInterval.add_ValueChanged({ $timer.Interval = [int]$numRefreshInterval.Value Update-StatusBar -Text ('Refresh interval set to {0} ms.' -f [int]$numRefreshInterval.Value) }) $comboAutoHold.add_SelectedIndexChanged({ if ($null -ne $comboAutoHold.SelectedItem) { $script:CpuAutoReleaseHoldSeconds = [int]$comboAutoHold.SelectedItem if ($script:CpuAutoEnabled) { Update-StatusBar -Text ('CPU auto release hold set to {0}s.' -f $script:CpuAutoReleaseHoldSeconds) } } }) $comboEnterThreshold.add_SelectedIndexChanged({ if ($null -ne $comboEnterThreshold.SelectedItem) { $script:CpuAutoEnterThreshold = [double][int]$comboEnterThreshold.SelectedItem if ($script:CpuAutoEnabled) { Update-StatusBar -Text ('CPU auto enter threshold set to {0}%. Exit is {1}%.' -f [int]$script:CpuAutoEnterThreshold, [int]$script:CpuAutoExitThreshold) } } }) $comboExitThreshold.add_SelectedIndexChanged({ if ($null -ne $comboExitThreshold.SelectedItem) { $script:CpuAutoExitThreshold = [double][int]$comboExitThreshold.SelectedItem if ($script:CpuAutoEnabled) { Update-StatusBar -Text ('CPU auto exit threshold set to {0}%. Enter is {1}%.' -f [int]$script:CpuAutoExitThreshold, [int]$script:CpuAutoEnterThreshold) } } }) $btnForce100.add_Click({ try { $timer.Stop() Enable-MaxFans Refresh-RpmStatus } catch { [System.Windows.Forms.MessageBox]::Show( $_.Exception.Message, $script:AppName, [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null } finally { $timer.Start() } }) $btnReleaseAuto.add_Click({ try { $timer.Stop() Release-ToAuto Refresh-RpmStatus } catch { [System.Windows.Forms.MessageBox]::Show( $_.Exception.Message, $script:AppName, [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null } finally { $timer.Start() } }) $btnCpuAuto.add_Click({ try { $timer.Stop() Toggle-CpuAutoMode } catch { [System.Windows.Forms.MessageBox]::Show( $_.Exception.Message, $script:AppName, [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null } finally { $timer.Start() } }) $btnResetPeaks.add_Click({ try { $timer.Stop() Reset-Peaks } catch { [System.Windows.Forms.MessageBox]::Show( $_.Exception.Message, $script:AppName, [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error ) | Out-Null } finally { $timer.Start() } }) $form.add_Shown({ try { Initialize-CpuCounter Initialize-AutoUiState $defaultHoldString = [string][int]$script:CpuAutoReleaseHoldDefault if (-not $comboAutoHold.Items.Contains($defaultHoldString)) { [void]$comboAutoHold.Items.Add($defaultHoldString) } $comboAutoHold.SelectedItem = $defaultHoldString $script:CpuAutoReleaseHoldSeconds = [int]$comboAutoHold.SelectedItem $defaultEnterString = [string][int]$script:CpuAutoEnterThresholdDefault if (-not $comboEnterThreshold.Items.Contains($defaultEnterString)) { [void]$comboEnterThreshold.Items.Add($defaultEnterString) } $comboEnterThreshold.SelectedItem = $defaultEnterString $script:CpuAutoEnterThreshold = [double][int]$comboEnterThreshold.SelectedItem $defaultExitString = [string][int]$script:CpuAutoExitThresholdDefault if (-not $comboExitThreshold.Items.Contains($defaultExitString)) { [void]$comboExitThreshold.Items.Add($defaultExitString) } $comboExitThreshold.SelectedItem = $defaultExitString $script:CpuAutoExitThreshold = [double][int]$comboExitThreshold.SelectedItem $script:LastCommand = 'Application started' $script:LastRefreshTime = Get-Date Update-MetaDisplay Refresh-RpmStatus $timer.Start() } catch { Update-StatusBar -Text $_.Exception.Message } }) $form.add_FormClosing({ try { $timer.Stop() } catch {} try { Restore-OriginalFanControlOnExit } catch {} try { if ($null -ne $script:CpuCounter) { $script:CpuCounter.Dispose() } } catch {} try { $script:FontUi.Dispose() $script:FontUiBold.Dispose() $script:FontTitle.Dispose() $script:FontCardTitle.Dispose() $script:FontValue.Dispose() } catch {} }) $form.ResumeLayout($false) [void]$form.ShowDialog()