# Complete Interactive Laptop Diagnostic Tool - Enhanced v15.1 # Version: 15.1 - Remote Execution & Hardware Detection Fixes # Developer: Hybrid Systems $ErrorActionPreference = "SilentlyContinue" # File-safe timestamp (used in report filename) $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' # Human-friendly timestamp (used in HTML display) $timestampDisplay = Get-Date -Format 'dd MMM yyyy, hh:mm tt' # Avoid repeated self-elevation loops for WMI auto-enable. $script:__wmiAutoElevateAttempted = $false # ============================================================ # PROGRESS LOADER FUNCTION # ============================================================ $script:totalSteps = 21 $script:currentStep = 0 function Show-Progress { param( [string]$Status, [int]$Step = -1 ) if ($Step -ge 0) { $script:currentStep = $Step } else { $script:currentStep++ } $percent = [math]::Round(($script:currentStep / $script:totalSteps) * 100) $barLength = 40 $filledLength = [math]::Round($barLength * $script:currentStep / $script:totalSteps) $emptyLength = $barLength - $filledLength $filledBar = [string]::new([char]0x2588, $filledLength) $emptyBar = [string]::new([char]0x2591, $emptyLength) # Clear line and write progress Write-Host "`r " -NoNewline Write-Host "`r [$filledBar$emptyBar] $percent% - $Status" -NoNewline -ForegroundColor Cyan } function Show-Header { Clear-Host Write-Host "" Write-Host " ============================================================" -ForegroundColor Magenta Write-Host " HARDWARE DIAGNOSTIC TOOL | HYBRID SYSTEMS" -ForegroundColor White Write-Host " ============================================================" -ForegroundColor Magenta Write-Host "" } function Show-Complete { Write-Host "`r " -NoNewline Write-Host "`r [" -NoNewline -ForegroundColor Green Write-Host "$([string]::new([char]0x2588, 40))" -NoNewline -ForegroundColor Green Write-Host "] 100% - Complete!" -ForegroundColor Green Write-Host "" Write-Host " ============================================================" -ForegroundColor Green Write-Host " DIAGNOSTIC COMPLETE - Opening Report..." -ForegroundColor White Write-Host " ============================================================" -ForegroundColor Green Write-Host "" } # Show header Show-Header # ============================================================ # REMOTE EXECUTION DETECTION # ============================================================ # Detect if script is running via irm | iex (remote execution) $isRemoteExecution = $false $scriptDir = $null $remoteServerUrl = "https://test.hybridsystems.org" # Configure your server URL here $remoteReportsPath = "C:\xampp\htdocs\reports" # Web-accessible directory on server $remoteReportsUrl = "$remoteServerUrl/reports" # Public URL for reports # Check multiple indicators for remote execution if ([string]::IsNullOrEmpty($MyInvocation.MyCommand.Path)) { $isRemoteExecution = $true } elseif ($MyInvocation.MyCommand.CommandType -eq "ExternalScript" -and -not (Test-Path $MyInvocation.MyCommand.Path -ErrorAction SilentlyContinue)) { $isRemoteExecution = $true } elseif ($MyInvocation.InvocationName -match "^Invoke-Expression|^iex$") { $isRemoteExecution = $true } # Set paths based on execution context (silent) if ($isRemoteExecution) { # For remote execution, try multiple possible web server paths $possiblePaths = @( $remoteReportsPath, "C:\xampp\htdocs\laptop-diagnostic-tool-main\reports", "C:\inetpub\wwwroot\reports", "C:\wamp\www\reports", "C:\wamp64\www\reports" ) $serverPathFound = $false foreach ($path in $possiblePaths) { try { if (-not (Test-Path $path)) { New-Item -ItemType Directory -Path $path -Force -ErrorAction Stop | Out-Null } $testFile = Join-Path $path ".write_test_$(Get-Random).tmp" [System.IO.File]::WriteAllText($testFile, "test") Remove-Item $testFile -Force -ErrorAction SilentlyContinue $outputDir = $path $scriptDir = Split-Path -Parent $path $serverPathFound = $true break } catch { continue } } if (-not $serverPathFound) { $outputDir = Join-Path ([Environment]::GetFolderPath("Desktop")) "DiagnosticReports" $scriptDir = [Environment]::GetFolderPath("Desktop") } } else { # Local execution - use script directory $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $outputDir = Join-Path $scriptDir "reports" } # Ensure output directory exists New-Item -ItemType Directory -Path $outputDir -Force | Out-Null # Initialize scoring variables $globalScore = 0 $globalScoreMax = 0 $performanceScore = 0 $scoreBreakdown = @() $testData = @{} # Load hardware database and upgrade recommendations # For remote execution, try to download from server or use embedded defaults $hardwareDB = $null $upgradeDB = $null if (-not $isRemoteExecution -and $scriptDir) { $hardwareDBPath = Join-Path $scriptDir "hardware_database.json" $upgradeDBPath = Join-Path $scriptDir "upgrade_recommendations.json" if (Test-Path $hardwareDBPath) { $hardwareDB = Get-Content $hardwareDBPath -Raw | ConvertFrom-Json } if (Test-Path $upgradeDBPath) { $upgradeDB = Get-Content $upgradeDBPath -Raw | ConvertFrom-Json } } else { # Remote execution: try to fetch from server try { $hardwareDB = Invoke-RestMethod "$remoteServerUrl/hardware_database.json" -ErrorAction SilentlyContinue } catch { # Silent fail - will use embedded defaults } try { $upgradeDB = Invoke-RestMethod "$remoteServerUrl/upgrade_recommendations.json" -ErrorAction SilentlyContinue } catch { # Silent fail - will use embedded defaults } } #region Helper Functions (keeping existing ones and adding new) function Ensure-WmiServiceRunning { # Best-effort: if WMI service is stopped/disabled, try to enable and start it. # This avoids blank/empty sections when Win10/11 has Winmgmt stopped. $result = @{ Running = $false; Started = $false; StartupType = "Unknown"; Message = ""; ElevationRequested = $false } try { $isAdmin = $false try { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { $isAdmin = $false } $svc = Get-Service -Name "Winmgmt" -ErrorAction Stop $result.StartupType = try { (Get-CimInstance -ClassName Win32_Service -Filter "Name='Winmgmt'" -ErrorAction Stop).StartMode } catch { "Unknown" } if ($svc.Status -ne "Running") { try { # If disabled, attempt to set to Automatic (requires admin; safe to fail). if ($result.StartupType -eq "Disabled") { if ($isAdmin) { try { Set-Service -Name "Winmgmt" -StartupType Automatic -ErrorAction Stop } catch { } } else { # Best-effort: attempt to relaunch the script elevated so we can enable/start WMI. if (-not $script:__wmiAutoElevateAttempted -and $MyInvocation -and $MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path -and (Test-Path $MyInvocation.MyCommand.Path -ErrorAction SilentlyContinue)) { $script:__wmiAutoElevateAttempted = $true try { $scriptPath = $MyInvocation.MyCommand.Path $argsList = "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`"" Start-Process -FilePath "powershell.exe" -ArgumentList $argsList -Verb RunAs | Out-Null $result.ElevationRequested = $true $result.Message = "WMI service is Disabled. Re-launching as Administrator to enable/start WMI (accept the UAC prompt)." return $result } catch { $result.Message = "WMI service is Disabled. Run this script as Administrator to enable/start Winmgmt automatically." } } else { $result.Message = "WMI service is Disabled. Run this script as Administrator to enable/start Winmgmt automatically." } } } Start-Service -Name "Winmgmt" -ErrorAction Stop $result.Started = $true } catch { $result.Message = "Unable to start WMI (Winmgmt): $($_.Exception.Message)" } } # Refresh status try { $svc = Get-Service -Name "Winmgmt" -ErrorAction Stop } catch { } $result.Running = ($svc -and $svc.Status -eq "Running") if (-not $result.Message) { $result.Message = if ($result.Running) { "WMI service is running" } else { "WMI service is not running" } } } catch { $result.Message = "WMI service (Winmgmt) not found or not accessible: $($_.Exception.Message)" } return $result } function Get-ApproxCpuTier { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name)) { return $null } $n = $Name # Apple Silicon if ($n -match "(?i)\bM([1-3])\b") { $mGen = [int]$matches[1] $year = if ($mGen -eq 1) { 2020 } elseif ($mGen -eq 2) { 2022 } else { 2023 } $base = if ($mGen -eq 1) { 70 } elseif ($mGen -eq 2) { 76 } else { 82 } $bonus = 0 if ($n -match "(?i)Ultra") { $bonus = 18 } elseif ($n -match "(?i)Max") { $bonus = 12 } elseif ($n -match "(?i)Pro") { $bonus = 8 } $score = [math]::Min(100, $base + $bonus) $tier = if ($score -ge 92) { "Flagship" } elseif ($score -ge 82) { "High Performance" } elseif ($score -ge 70) { "Mainstream" } else { "Mid-Range" } return @{ Tier = $tier; Score = [int]$score; Category = "CPU"; EstimatedYear = $year } } # Intel Xeon W-series (common workstation naming: "Intel(R) Xeon(R) W-2235 CPU @ ...") if ($n -match "(?i)\bXeon\b.*\bW-?(\d{4})\b") { $model = 0 try { $model = [int]$matches[1] } catch { $model = 0 } # Coarse series mapping (best-effort fallback). Prefer database patterns when available. # NOTE: W-2235 (6C/12T) is a solid workstation CPU but not a flagship-tier part. $score = 80 $year = 2019 if ($model -ge 3400) { $score = 96; $year = 2024 } elseif ($model -ge 2400) { $score = 94; $year = 2023 } elseif ($model -ge 2200) { $year = 2019 if ($model -ge 2270) { $score = 85 } else { $score = 70 } } elseif ($model -ge 2100) { $score = 75; $year = 2017 } elseif ($model -ge 1300) { $score = 80; $year = 2021 } elseif ($model -ge 1200) { $score = 76; $year = 2020 } $tier = if ($score -ge 92) { "Flagship" } elseif ($score -ge 82) { "High Performance" } elseif ($score -ge 70) { "Mainstream" } elseif ($score -ge 55) { "Mid-Range" } elseif ($score -ge 40) { "Entry Level" } else { "Legacy" } return @{ Tier = $tier; Score = [int]$score; Category = "CPU"; EstimatedYear = $year } } # Intel Core i3/i5/i7/i9 if ($n -match "(?i)\b(i[3579])[-\s]?(\d{4,5})([A-Za-z0-9]{0,6})\b") { $cls = $matches[1].ToLower() $digits = $matches[2] $suffix = $matches[3] $gen = 0 if ($digits.Length -ge 5) { $gen = [int]$digits.Substring(0,2) } elseif ($digits.Length -eq 4) { $first2 = [int]$digits.Substring(0,2) $gen = if ($first2 -ge 10) { $first2 } else { [int]$digits.Substring(0,1) } } $genBase = switch ($gen) { 14 { 78 } 13 { 76 } 12 { 73 } 11 { 70 } 10 { 67 } 9 { 64 } 8 { 61 } 7 { 56 } 6 { 52 } 5 { 48 } 4 { 42 } 3 { 35 } 2 { 28 } default { 22 } } $classBonus = switch ($cls) { "i9" { 18 } "i7" { 12 } "i5" { 6 } default { 0 } } $suffixAdj = 0 if ($suffix -match "(?i)HX") { $suffixAdj += 4 } elseif ($suffix -match "(?i)H") { $suffixAdj += 3 } elseif ($suffix -match "(?i)P") { $suffixAdj += 1 } if ($suffix -match "(?i)K") { $suffixAdj += 2 } if ($suffix -match "(?i)U") { $suffixAdj -= 2 } if ($suffix -match "(?i)Y") { $suffixAdj -= 3 } $minor = 0 try { $last2 = [int]$digits.Substring($digits.Length-2, 2) $minor = [math]::Round((($last2 / 99.0) * 4.0) - 2.0) } catch { } $score = [math]::Max(0, [math]::Min(100, ($genBase + $classBonus + $suffixAdj + $minor))) $year = switch ($gen) { 14 { 2023 } 13 { 2022 } 12 { 2021 } 11 { 2020 } 10 { 2019 } 9 { 2018 } 8 { 2017 } 7 { 2016 } 6 { 2015 } 5 { 2014 } 4 { 2013 } 3 { 2012 } 2 { 2011 } default { 2010 } } $tier = if ($score -ge 92) { "Flagship" } elseif ($score -ge 82) { "High Performance" } elseif ($score -ge 70) { "Mainstream" } elseif ($score -ge 55) { "Mid-Range" } elseif ($score -ge 40) { "Entry Level" } else { "Legacy" } return @{ Tier = $tier; Score = [int]$score; Category = "CPU"; EstimatedYear = $year } } # AMD Ryzen if ($n -match "(?i)\bRyzen\s+(3|5|7|9)\s+(\d{4,5})([A-Za-z0-9]{0,6})\b") { $cls = [int]$matches[1] $digits = $matches[2] $suffix = $matches[3] $fam = [int]$digits.Substring(0,1) # 1xxx..9xxx $famBase = switch ($fam) { 9 { 84 } 8 { 82 } 7 { 78 } 6 { 74 } 5 { 70 } 4 { 66 } 3 { 60 } 2 { 54 } 1 { 48 } default { 40 } } $classBonus = switch ($cls) { 9 { 14 } 7 { 9 } 5 { 5 } default { 0 } } $suffixAdj = 0 if ($suffix -match "(?i)X3D") { $suffixAdj += 4 } elseif ($suffix -match "(?i)X") { $suffixAdj += 2 } if ($suffix -match "(?i)U") { $suffixAdj -= 2 } if ($suffix -match "(?i)H") { $suffixAdj += 2 } $minor = 0 try { $last2 = [int]$digits.Substring($digits.Length-2, 2) $minor = [math]::Round((($last2 / 99.0) * 4.0) - 2.0) } catch { } $score = [math]::Max(0, [math]::Min(100, ($famBase + $classBonus + $suffixAdj + $minor))) $year = switch ($fam) { 9 { 2024 } 8 { 2024 } 7 { 2022 } 6 { 2020 } 5 { 2020 } 4 { 2019 } 3 { 2017 } 2 { 2018 } 1 { 2017 } default { 2016 } } $tier = if ($score -ge 92) { "Flagship" } elseif ($score -ge 82) { "High Performance" } elseif ($score -ge 70) { "Mainstream" } elseif ($score -ge 55) { "Mid-Range" } elseif ($score -ge 40) { "Entry Level" } else { "Legacy" } return @{ Tier = $tier; Score = [int]$score; Category = "CPU"; EstimatedYear = $year } } # Legacy families (very coarse) if ($n -match "(?i)\b(Core\s*2|Athlon|Phenom|FX[-\s]?\d|A\d-\d)\b") { return @{ Tier = "Legacy"; Score = 20; Category = "CPU"; EstimatedYear = 2010 } } return $null } function Get-ApproxGpuTier { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name)) { return $null } $n = $Name # NVIDIA RTX #### (+Ti/+SUPER) if ($n -match "(?i)\bRTX\s*(\d{4})\b") { $model = [int]$matches[1] $series = [int]([math]::Floor($model / 100)) $cls = $model % 100 $base = switch ($series) { 50 { 92 } 40 { 88 } 30 { 80 } 20 { 70 } default { 65 } } $adj = switch ($cls) { 90 { 10 } 80 { 7 } 70 { 5 } 60 { 2 } 50 { -2 } 40 { -5 } 30 { -8 } default { -10 } } if ($n -match "(?i)Ti") { $adj += 2 } if ($n -match "(?i)SUPER") { $adj += 1 } $score = [math]::Max(0, [math]::Min(100, $base + $adj)) $year = switch ($series) { 50 { 2025 } 40 { 2022 } 30 { 2020 } 20 { 2018 } default { 2017 } } $tier = if ($score -ge 95) { "Flagship" } elseif ($score -ge 86) { "High Performance" } elseif ($score -ge 76) { "Performance" } elseif ($score -ge 62) { "Mainstream" } elseif ($score -ge 45) { "Entry Level" } else { "Budget" } return @{ Tier = $tier; Score = [int]$score; Category = "GPU"; EstimatedYear = $year } } # NVIDIA GTX #### / ### if ($n -match "(?i)\bGTX\s*(\d{3,4})\b") { $model = [int]$matches[1] $series = if ($model -ge 1000) { [int]([math]::Floor($model / 100)) } else { [int]([math]::Floor($model / 10)) } $cls = $model % 100 $base = switch ($series) { 16 { 62 } 10 { 60 } 9 { 52 } 7 { 45 } 6 { 38 } default { 32 } } $adj = switch ($cls) { 80 { 5 } 70 { 3 } 60 { 1 } 50 { -2 } default { -5 } } if ($n -match "(?i)Ti") { $adj += 2 } $score = [math]::Max(0, [math]::Min(100, $base + $adj)) $year = if ($series -ge 16) { 2019 } elseif ($series -ge 10) { 2016 } elseif ($series -ge 9) { 2014 } else { 2013 } $tier = if ($score -ge 76) { "Performance" } elseif ($score -ge 62) { "Mainstream" } elseif ($score -ge 45) { "Entry Level" } else { "Budget" } return @{ Tier = $tier; Score = [int]$score; Category = "GPU"; EstimatedYear = $year } } # AMD Radeon RX #### / ##### (+XT/+XTX) if ($n -match "(?i)\bRX\s*(\d{4,5})\b") { $digits = $matches[1] $fam = [int]$digits.Substring(0,1) # 4/5/6/7 $base = switch ($fam) { 7 { 86 } 6 { 78 } 5 { 68 } 4 { 55 } default { 45 } } $adj = 0 if ($n -match "(?i)XTX") { $adj += 5 } elseif ($n -match "(?i)XT") { $adj += 3 } $last2 = 0 try { $last2 = [int]$digits.Substring($digits.Length-2,2) } catch { } if ($last2 -ge 80) { $adj += 5 } elseif ($last2 -ge 70) { $adj += 3 } elseif ($last2 -ge 60) { $adj += 1 } elseif ($last2 -ge 50) { $adj += 0 } else { $adj -= 2 } $score = [math]::Max(0, [math]::Min(100, $base + $adj)) $year = switch ($fam) { 7 { 2022 } 6 { 2020 } 5 { 2019 } 4 { 2016 } default { 2015 } } $tier = if ($score -ge 95) { "Flagship" } elseif ($score -ge 86) { "High Performance" } elseif ($score -ge 76) { "Performance" } elseif ($score -ge 62) { "Mainstream" } elseif ($score -ge 45) { "Entry Level" } else { "Budget" } return @{ Tier = $tier; Score = [int]$score; Category = "GPU"; EstimatedYear = $year } } # Intel Arc if ($n -match "(?i)\bArc\s*A(\d{3,4})\b") { $m = [int]$matches[1] $score = if ($m -ge 750) { 68 } elseif ($m -ge 700) { 64 } elseif ($m -ge 580) { 58 } elseif ($m -ge 380) { 42 } else { 35 } $tier = if ($score -ge 62) { "Mainstream" } elseif ($score -ge 45) { "Entry Level" } else { "Budget" } return @{ Tier = $tier; Score = $score; Category = "GPU"; EstimatedYear = 2022 } } # Integrated (coarse) if ($n -match "(?i)Iris\s*Xe") { return @{ Tier = "Integrated Plus"; Score = 38; Category = "GPU"; EstimatedYear = 2020 } } if ($n -match "(?i)Iris\s*Plus") { return @{ Tier = "Integrated"; Score = 32; Category = "GPU"; EstimatedYear = 2019 } } if ($n -match "(?i)UHD|Intel\s*HD") { return @{ Tier = "Integrated"; Score = 22; Category = "GPU"; EstimatedYear = 2017 } } if ($n -match "(?i)Vega\s*(\d+)|Radeon\s+Graphics") { return @{ Tier = "Integrated"; Score = 28; Category = "GPU"; EstimatedYear = 2019 } } return $null } # --- Exact-match benchmark lookups (optional; populated by Update-HardwareDatabase.ps1) --- $script:__exactMapsInitialized = $false $script:__cpuExactMap = @{} $script:__gpuExactMap = @{} $script:__storageExactMap = @{} $script:__ramExactMap = @{} $script:__ramBinsByType = @{} function Normalize-HardwareName { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name)) { return "" } $s = ($Name -as [string]).ToUpperInvariant() # Remove common vendor/noise tokens so that e.g. "NVIDIA GeForce RTX 4090" matches "GeForce RTX 4090". $s = $s -replace "\(R\)|\(TM\)", "" $s = $s -replace "\bNVIDIA\b|\bAMD\b|\bATI\b|\bINTEL\b|\bCORPORATION\b|\bINC\b", "" $s = $s -replace "\bGRAPHICS\b|\bADAPTER\b|\bMOBILE\b|\bLAPTOP\b|\bWITH\b", "" # Strip all non-alphanumerics for robust matching. $s = $s -replace "[^A-Z0-9]+", "" return $s } # Storage names vary heavily between WMI and benchmark sites (vendor order, NVMe tokens, capacity units). # This normalizer is used only for *storage* matching as a fallback key. function Normalize-StorageMatchKey { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name)) { return "" } $s = ($Name -as [string]).ToUpperInvariant() $s = $s -replace "\(R\)|\(TM\)", "" # Remove common manufacturer tokens $s = $s -replace "\bSAMSUNG\b|\bSEAGATE\b|\bTOSHIBA\b|\bHGST\b|\bHITACHI\b|\bWESTERN\s*DIGITAL\b|\bWDC\b|\bSANDISK\b|\bKINGSTON\b|\bMICRON\b|\bCRUCIAL\b|\bSK\s*HYNIX\b|\bHYNIX\b|\bINTEL\b|\bADATA\b|\bCORSAIR\b|\bTEAMGROUP\b|\bPNY\b", "" # Remove technology/capacity noise $s = $s -replace "\bNVME\b|\bNVM\s*E\b|\bSSD\b|\bHDD\b|\bM\.2\b|\bPCIE\b|\bPCI\s*E\b|\bSATA\b|\bSAS\b|\bUSB\b", "" $s = $s -replace "\b[0-9]+\s*(GB|TB)\b", "" $s = $s -replace "[^A-Z0-9]+", "" return $s } function Get-DisplayConnectorCounts { param([string]$ConnectorString) $counts = @{ HDMI = 0; DisplayPort = 0; DVI = 0; VGA = 0; USBC = 0; Total = 0; Source = "None" } if ([string]::IsNullOrWhiteSpace($ConnectorString)) { return $counts } $counts.Source = "DBGPU" $s = ("" + $ConnectorString) $parts = $s -split "[,;/\r\n]+" | ForEach-Object { $_.Trim() } | Where-Object { $_ } foreach ($p in $parts) { $seg = ("" + $p) $n = 1 if ($seg -match '(?i)\bx\s*([0-9]+)\b') { $n = [int]$Matches[1] } elseif ($seg -match '(?i)\b([0-9]+)\s*x\b') { $n = [int]$Matches[1] } if ($seg -match '(?i)HDMI') { $counts.HDMI += $n } elseif ($seg -match '(?i)DISPLAY\s*PORT|MINI\s*DP|\bMDP\b') { $counts.DisplayPort += $n } elseif ($seg -match '(?i)\bDVI\b') { $counts.DVI += $n } elseif ($seg -match '(?i)\bVGA\b|HD15|D-?SUB') { $counts.VGA += $n } elseif ($seg -match '(?i)USB\s*-?\s*C|TYPE\s*-?\s*C|VIRTUALLINK') { $counts.USBC += $n } } $counts.Total = ($counts.HDMI + $counts.DisplayPort + $counts.DVI + $counts.VGA + $counts.USBC) return $counts } function Initialize-HardwareDatabaseExactMaps { param([object]$Database) $script:__cpuExactMap = @{} $script:__gpuExactMap = @{} $script:__storageExactMap = @{} $script:__ramExactMap = @{} $script:__ramBinsByType = @{} if (-not $Database) { return } # CPU exact map try { $cpuExact = $null if ($Database.cpus -and $Database.cpus.exact) { $cpuExact = $Database.cpus.exact } elseif ($Database.cpus_exact) { $cpuExact = $Database.cpus_exact } if ($cpuExact) { if ($cpuExact -is [System.Collections.IDictionary]) { foreach ($k in $cpuExact.Keys) { $nk = Normalize-HardwareName -Name ("" + $k) if ($nk) { $script:__cpuExactMap[$nk] = $cpuExact[$k] } } } else { foreach ($p in $cpuExact.PSObject.Properties) { if ($p.MemberType -ne 'NoteProperty') { continue } # Primary key: property name $nk = Normalize-HardwareName -Name ("" + $p.Name) if ($nk) { $script:__cpuExactMap[$nk] = $p.Value } # Secondary key: embedded display name (if present) $displayName = $null try { $displayName = (("" + $p.Value.name).Trim()) } catch { $displayName = $null } if ($displayName) { $nk2 = Normalize-HardwareName -Name $displayName if ($nk2 -and -not $script:__cpuExactMap.ContainsKey($nk2)) { $script:__cpuExactMap[$nk2] = $p.Value } } } } } } catch { } # GPU exact map (supports legacy DB key: gpu) try { $gpuExact = $null if ($Database.gpus -and $Database.gpus.exact) { $gpuExact = $Database.gpus.exact } elseif ($Database.gpu -and $Database.gpu.exact) { $gpuExact = $Database.gpu.exact } elseif ($Database.gpus_exact) { $gpuExact = $Database.gpus_exact } if ($gpuExact) { if ($gpuExact -is [System.Collections.IDictionary]) { foreach ($k in $gpuExact.Keys) { $nk = Normalize-HardwareName -Name ("" + $k) if ($nk) { $script:__gpuExactMap[$nk] = $gpuExact[$k] } } } else { foreach ($p in $gpuExact.PSObject.Properties) { if ($p.MemberType -ne 'NoteProperty') { continue } # Primary key: property name $nk = Normalize-HardwareName -Name ("" + $p.Name) if ($nk) { $script:__gpuExactMap[$nk] = $p.Value } # Secondary key: embedded display name (if present) $displayName = $null try { $displayName = (("" + $p.Value.name).Trim()) } catch { $displayName = $null } if ($displayName) { $nk2 = Normalize-HardwareName -Name $displayName if ($nk2 -and -not $script:__gpuExactMap.ContainsKey($nk2)) { $script:__gpuExactMap[$nk2] = $p.Value } } } } } } catch { } # Storage exact map try { $storageExact = $null if ($Database.storage -and $Database.storage.exact) { $storageExact = $Database.storage.exact } elseif ($Database.storage_exact) { $storageExact = $Database.storage_exact } if ($storageExact) { if ($storageExact -is [System.Collections.IDictionary]) { foreach ($k in $storageExact.Keys) { $nk = Normalize-HardwareName -Name ("" + $k) if ($nk) { $script:__storageExactMap[$nk] = $storageExact[$k] } # Fallback alias key: vendor/capacity-agnostic try { $ak = Normalize-StorageMatchKey -Name ("" + $k) if ($ak) { if (-not $script:__storageExactMap.ContainsKey($ak)) { $script:__storageExactMap[$ak] = $storageExact[$k] } else { $cur = $script:__storageExactMap[$ak] $curScore = try { [int]$cur.score } catch { try { [int]$cur.Score } catch { 0 } } $newScore = try { [int]$storageExact[$k].score } catch { try { [int]$storageExact[$k].Score } catch { 0 } } if ($newScore -gt $curScore) { $script:__storageExactMap[$ak] = $storageExact[$k] } } } } catch { } } } else { foreach ($p in $storageExact.PSObject.Properties) { if ($p.MemberType -ne 'NoteProperty') { continue } $nk = Normalize-HardwareName -Name ("" + $p.Name) if ($nk) { $script:__storageExactMap[$nk] = $p.Value } # Fallback alias key: vendor/capacity-agnostic try { $ak = Normalize-StorageMatchKey -Name ("" + $p.Name) if ($ak) { if (-not $script:__storageExactMap.ContainsKey($ak)) { $script:__storageExactMap[$ak] = $p.Value } else { $cur = $script:__storageExactMap[$ak] $curScore = try { [int]$cur.score } catch { try { [int]$cur.Score } catch { 0 } } $newScore = try { [int]$p.Value.score } catch { try { [int]$p.Value.Score } catch { 0 } } if ($newScore -gt $curScore) { $script:__storageExactMap[$ak] = $p.Value } } } } catch { } $displayName = $null try { $displayName = (("" + $p.Value.name).Trim()) } catch { $displayName = $null } if ($displayName) { $nk2 = Normalize-HardwareName -Name $displayName if ($nk2 -and -not $script:__storageExactMap.ContainsKey($nk2)) { $script:__storageExactMap[$nk2] = $p.Value } # Alias from embedded name as well try { $ak2 = Normalize-StorageMatchKey -Name $displayName if ($ak2) { if (-not $script:__storageExactMap.ContainsKey($ak2)) { $script:__storageExactMap[$ak2] = $p.Value } else { $cur = $script:__storageExactMap[$ak2] $curScore = try { [int]$cur.score } catch { try { [int]$cur.Score } catch { 0 } } $newScore = try { [int]$p.Value.score } catch { try { [int]$p.Value.Score } catch { 0 } } if ($newScore -gt $curScore) { $script:__storageExactMap[$ak2] = $p.Value } } } } catch { } } } } } } catch { } # RAM exact map try { $ramExact = $null if ($Database.ram -and $Database.ram.exact) { $ramExact = $Database.ram.exact } elseif ($Database.ram_exact) { $ramExact = $Database.ram_exact } if ($ramExact) { if ($ramExact -is [System.Collections.IDictionary]) { foreach ($k in $ramExact.Keys) { $nk = Normalize-HardwareName -Name ("" + $k) if ($nk) { $script:__ramExactMap[$nk] = $ramExact[$k] } } } else { foreach ($p in $ramExact.PSObject.Properties) { if ($p.MemberType -ne 'NoteProperty') { continue } $nk = Normalize-HardwareName -Name ("" + $p.Name) if ($nk) { $script:__ramExactMap[$nk] = $p.Value } $displayName = $null try { $displayName = (("" + $p.Value.name).Trim()) } catch { $displayName = $null } if ($displayName) { $nk2 = Normalize-HardwareName -Name $displayName if ($nk2 -and -not $script:__ramExactMap.ContainsKey($nk2)) { $script:__ramExactMap[$nk2] = $p.Value } } } } } } catch { } # RAM bins (PassMark-derived summaries by DDR type + speed) try { $ramBins = $null if ($Database.ram -and $Database.ram.bins) { $ramBins = $Database.ram.bins } elseif ($Database.ram_bins) { $ramBins = $Database.ram_bins } if ($ramBins) { foreach ($b in @($ramBins)) { $ddr = $null try { $ddr = ("" + $b.ddrType).Trim() } catch { $ddr = $null } if (-not $ddr) { try { $ddr = ("" + $b.DDRType).Trim() } catch { $ddr = $null } } if (-not $ddr) { continue } $ddr = $ddr.ToUpperInvariant() if (-not $script:__ramBinsByType.ContainsKey($ddr)) { $script:__ramBinsByType[$ddr] = @() } $script:__ramBinsByType[$ddr] += $b } } } catch { } } function Get-HardwareTier { param([string]$ComponentType, [object]$ComponentName, [object]$Database) if (-not $Database) { return @{ Tier = "Unknown"; Score = 50; Category = "N/A" } } # Lazy-init exact maps once per run to avoid repeated JSON object enumeration. if (-not $script:__exactMapsInitialized) { Initialize-HardwareDatabaseExactMaps -Database $Database $script:__exactMapsInitialized = $true } $result = @{ Tier = "Unknown"; Score = 50; Category = "N/A"; EstimatedYear = 2020 } switch ($ComponentType) { "CPU" { # Prefer exact-match benchmark-derived entries if present in DB. # Try a few name variants because Win32_Processor.Name often includes tokens like "CPU" and "@ 2.30GHz", # while benchmark datasets may not. try { $candidates = @() if ($ComponentName) { $candidates += ("" + $ComponentName) } # Remove common suffix tokens try { $candidates += ((("" + $ComponentName) -replace '(?i)\bCPU\b|\bPROCESSOR\b', '').Trim()) } catch { } # Remove anything after '@' (frequency) try { $candidates += ((("" + $ComponentName) -split '@')[0].Trim()) } catch { } # Both: remove tokens, then strip frequency try { $tmp = (("" + $ComponentName) -replace '(?i)\bCPU\b|\bPROCESSOR\b', '') $candidates += (($tmp -split '@')[0].Trim()) } catch { } foreach ($cand in ($candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)) { $nk = Normalize-HardwareName -Name $cand if ($nk -and $script:__cpuExactMap.ContainsKey($nk)) { $e = $script:__cpuExactMap[$nk] $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { $null } if (-not $cat) { $cat = try { $e.Category } catch { "CPU" } } $yr = try { $e.releaseYear } catch { $null } if ($null -eq $yr) { $yr = try { $e.EstimatedYear } catch { 2020 } } # PowerShell 5.1 compatibility: avoid null-coalescing operator (??) $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } $catOut = if ($cat) { $cat } else { "CPU" } $yrOut = if ($null -ne $yr) { [int]$yr } else { 2020 } $bench = $null $sources = $null $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } return @{ Tier = $tierOut; Score = $scoreOut; Category = $catOut; EstimatedYear = $yrOut; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } } catch { } $matchedPattern = $null if ($Database.cpus -and $Database.cpus.patterns) { foreach ($pattern in $Database.cpus.patterns) { if ($ComponentName -imatch $pattern.pattern) { $result.Tier = $pattern.tier $result.Score = $pattern.score $result.Category = $pattern.category $result.EstimatedYear = $pattern.releaseYear $matchedPattern = $pattern break } } } # Add granularity even when DB patterns are broad $approxCpu = Get-ApproxCpuTier -Name ("" + $ComponentName) if ($approxCpu) { if ($result.Tier -eq "Unknown") { $result = $approxCpu } else { # If the DB provides a min/max band, keep the score within it if ($matchedPattern -and $null -ne $matchedPattern.minScore -and $null -ne $matchedPattern.maxScore) { $minS = [int]$matchedPattern.minScore $maxS = [int]$matchedPattern.maxScore $result.Score = [int]([math]::Max($minS, [math]::Min($maxS, [int]$approxCpu.Score))) } else { $result.Score = [int]$approxCpu.Score } } } } "GPU" { # Prefer exact-match benchmark-derived entries if present in DB. try { $nk = Normalize-HardwareName -Name ("" + $ComponentName) if ($nk -and $script:__gpuExactMap.ContainsKey($nk)) { $e = $script:__gpuExactMap[$nk] $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { $null } if (-not $cat) { $cat = try { $e.Category } catch { "GPU" } } $yr = try { $e.releaseYear } catch { $null } if ($null -eq $yr) { $yr = try { $e.EstimatedYear } catch { 2020 } } # PowerShell 5.1 compatibility: avoid null-coalescing operator (??) $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } $catOut = if ($cat) { $cat } else { "GPU" } $yrOut = if ($null -ne $yr) { [int]$yr } else { 2020 } $bench = $null $sources = $null $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } return @{ Tier = $tierOut; Score = $scoreOut; Category = $catOut; EstimatedYear = $yrOut; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } catch { } $matched = $false $matchedPattern = $null # Backward compatibility: allow DB key to be either 'gpus' or legacy 'gpu' $gpuDb = $null if ($Database.gpus) { $gpuDb = $Database.gpus } elseif ($Database.gpu) { $gpuDb = $Database.gpu } if ($gpuDb -and $gpuDb.patterns) { foreach ($pattern in $gpuDb.patterns) { if ($ComponentName -imatch $pattern.pattern) { $result.Tier = $pattern.tier $result.Score = $pattern.score $result.Category = $pattern.category $result.EstimatedYear = $pattern.releaseYear $matched = $true $matchedPattern = $pattern break } } } # Add granularity to GPUs by parsing the model number $approxGpu = Get-ApproxGpuTier -Name ("" + $ComponentName) if ($approxGpu) { if (-not $matched) { $result = $approxGpu $matched = $true } else { # Keep within a small band around the DB score to preserve tier intent $low = [math]::Max(0, [int]$matchedPattern.score - 8) $high = [math]::Min(100, [int]$matchedPattern.score + 8) $result.Score = [int]([math]::Max($low, [math]::Min($high, [int]$approxGpu.Score))) } } # Fallback patterns for common GPUs not in database (inline detection) if (-not $matched) { $fallbackPatterns = @( @{ Pattern = "RTX 5090|RTX 5080|RTX 4090|RTX 4080|Quadro RTX 8000|Quadro RTX 6000"; Tier = "Flagship"; Score = 98 }, @{ Pattern = "Quadro RTX 4000|Quadro RTX 5000|RTX 4070|RTX 5070|RTX A4000|RTX A4500"; Tier = "High Performance"; Score = 90 }, @{ Pattern = "Quadro RTX 3000|Quadro P5000|RTX 4060|RTX 3080|RTX 3070|RTX A2000"; Tier = "Performance"; Score = 80 }, @{ Pattern = "Quadro P4000|Quadro P3200|Quadro M5000|RTX 3060|RTX 3050"; Tier = "Mainstream"; Score = 70 }, @{ Pattern = "Quadro P2200|Quadro P2000|Quadro M4000|RTX 2080|RTX 2070|RTX 2060|GTX 1080|GTX 1070"; Tier = "Mid-Range"; Score = 60 }, @{ Pattern = "Quadro P1000|Quadro P620|Quadro P600|Quadro M2000|GTX 1660|GTX 1650|GTX 1060|GTX 1050"; Tier = "Entry Level"; Score = 45 }, @{ Pattern = "Quadro K|GTX 980|GTX 970|GTX 960|RX 560|RX 550"; Tier = "Budget"; Score = 35 }, @{ Pattern = "Intel.*Iris.*Xe|Intel.*Arc"; Tier = "Integrated Plus"; Score = 40 }, @{ Pattern = "Intel.*UHD|Intel.*HD|AMD.*Vega|AMD.*Graphics"; Tier = "Integrated"; Score = 25 } ) foreach ($fb in $fallbackPatterns) { if ($ComponentName -imatch $fb.Pattern) { $result.Tier = $fb.Tier $result.Score = $fb.Score $result.Category = "GPU" break } } } } "RAM" { # Prefer exact-match benchmark-derived entries if present in DB. try { $nk = Normalize-HardwareName -Name ("" + $ComponentName) if ($nk -and $script:__ramExactMap.ContainsKey($nk)) { $e = $script:__ramExactMap[$nk] $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { $null } if (-not $cat) { $cat = try { $e.Category } catch { "RAM" } } $yr = try { $e.releaseYear } catch { $null } if ($null -eq $yr) { $yr = try { $e.EstimatedYear } catch { 2020 } } $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } $catOut = if ($cat) { $cat } else { "RAM" } $yrOut = if ($null -ne $yr) { [int]$yr } else { 2020 } $bench = $null $sources = $null $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } # PassMark RAM dataset stores DDR type as a top-level field (ddrType), not under specs if (-not $specs) { try { $ddr = $null try { $ddr = ("" + $e.ddrType).Trim() } catch { $ddr = $null } if ($ddr) { $specs = [pscustomobject]@{ DDRType = $ddr } } } catch { } } return @{ Tier = $tierOut; Score = $scoreOut; Category = $catOut; EstimatedYear = $yrOut; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } catch { } # Fallback: PassMark RAM bins (DDR type + speed MHz). This is used when WMI names/part numbers # don't match PassMark's marketing module names. try { if ($ComponentName -is [System.Collections.IDictionary]) { $ddr = $null $spd = 0 try { $ddr = ("" + $ComponentName.DDRType).Trim() } catch { $ddr = $null } if (-not $ddr) { try { $ddr = ("" + $ComponentName.Type).Trim() } catch { $ddr = $null } } if ($ddr) { $ddr = $ddr.ToUpperInvariant() } try { $spd = [int](("" + $ComponentName.SpeedMHz) -replace '[^0-9]', '') } catch { $spd = 0 } if ($spd -le 0) { try { $spd = [int](("" + $ComponentName.Speed) -replace '[^0-9]', '') } catch { $spd = 0 } } if ($ddr -and $script:__ramBinsByType.ContainsKey($ddr)) { $bins = @($script:__ramBinsByType[$ddr]) if ($bins -and $bins.Count -gt 0) { $best = $null if ($spd -gt 0) { foreach ($b in $bins) { $bSpd = 0 try { $bSpd = [int]$b.speedMHz } catch { try { $bSpd = [int]$b.SpeedMHz } catch { $bSpd = 0 } } if ($bSpd -le 0) { continue } $diff = [math]::Abs($bSpd - $spd) if (-not $best -or $diff -lt $best.diff) { $best = @{ item = $b; diff = $diff; speedMHz = $bSpd } } } if ($best -and $best.diff -le 600) { $e = $best.item $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { $null } if (-not $cat) { $cat = "PassMark RAM (bin)" } $bench = $null; $sources = $null; $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } if (-not $specs) { $specs = [pscustomobject]@{ DDRType = $ddr; SpeedMHz = $best.speedMHz } } $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } return @{ Tier = $tierOut; Score = $scoreOut; Category = $cat; EstimatedYear = 2020; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } # If speed is unknown/unreliable, choose the most representative bin by sample count. if ($spd -le 0) { $e = $bins | Sort-Object { try { [int]$_.sampleCount } catch { 0 } } -Descending | Select-Object -First 1 if ($e) { $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { "PassMark RAM (bin)" } $bench = $null; $sources = $null; $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } if (-not $specs) { $specs = [pscustomobject]@{ DDRType = $ddr } } $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } return @{ Tier = $tierOut; Score = $scoreOut; Category = $cat; EstimatedYear = 2020; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } } } } } catch { } # Allow both "16GB" and "16 GB" formats if ($ComponentName -match "(\d+)\s*GB.*?(DDR\d+)") { $ramGB = [int]$matches[1] $ramType = $matches[2] if ($Database.ram -and $Database.ram.capacity) { foreach ($tier in $Database.ram.capacity) { $tierType = $tier.type $tierMinGB = $tier.minGB if (($tierType -eq "any" -or $ramType -eq $tierType -or ($tierType -eq "any" -and $ramGB -ge $tierMinGB)) -and $ramGB -ge $tierMinGB) { $result.Tier = $tier.tier $result.Score = $tier.score $result.Category = $tier.category break } } } } } "Storage" { $modelName = $null $legacyStr = $null try { if ($ComponentName -is [System.Collections.IDictionary]) { try { $modelName = ("" + $ComponentName.Model).Trim() } catch { $modelName = $null } try { $legacyStr = ("" + $ComponentName.LegacyString).Trim() } catch { $legacyStr = $null } } else { $modelName = ("" + $ComponentName) } } catch { } # Prefer PassMark DiskMark exact entries if present in DB try { $nk = Normalize-HardwareName -Name $modelName if ($nk -and $script:__storageExactMap.ContainsKey($nk)) { $e = $script:__storageExactMap[$nk] $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { $null } if (-not $cat) { $cat = try { $e.Category } catch { "Storage" } } $yr = try { $e.releaseYear } catch { $null } if ($null -eq $yr) { $yr = try { $e.EstimatedYear } catch { 2020 } } $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } $catOut = if ($cat) { $cat } else { "Storage" } $yrOut = if ($null -ne $yr) { [int]$yr } else { 2020 } $bench = $null $sources = $null $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } # PassMark Storage dataset stores basic fields at top-level (type/size), not under specs if (-not $specs) { try { $stType = $null $stSize = $null try { $stType = ("" + $e.type).Trim() } catch { $stType = $null } try { $stSize = ("" + $e.size).Trim() } catch { $stSize = $null } if ($stType -or $stSize) { $specs = [pscustomobject]@{ Type = $stType; Size = $stSize } } } catch { } } return @{ Tier = $tierOut; Score = $scoreOut; Category = $catOut; EstimatedYear = $yrOut; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } catch { } # Fallback: try vendor/capacity-agnostic key (helps WMI strings like "PM981a NVMe SAMSUNG 2048GB") try { $ak = Normalize-StorageMatchKey -Name $modelName if ($ak -and $script:__storageExactMap.ContainsKey($ak)) { $e = $script:__storageExactMap[$ak] $tier = try { $e.tier } catch { $null } if (-not $tier) { $tier = try { $e.Tier } catch { $null } } $score = try { $e.score } catch { $null } if ($null -eq $score) { $score = try { $e.Score } catch { $null } } $cat = try { $e.category } catch { $null } if (-not $cat) { $cat = try { $e.Category } catch { "Storage" } } $yr = try { $e.releaseYear } catch { $null } if ($null -eq $yr) { $yr = try { $e.EstimatedYear } catch { 2020 } } $tierOut = if ($tier) { $tier } else { "Unknown" } $scoreOut = if ($null -ne $score) { [int]$score } else { 50 } $catOut = if ($cat) { $cat } else { "Storage" } $yrOut = if ($null -ne $yr) { [int]$yr } else { 2020 } $bench = $null $sources = $null $specs = $null try { $bench = $e.benchmarks } catch { $bench = $null } try { $sources = $e.sources } catch { $sources = $null } try { $specs = $e.specs } catch { $specs = $null } # PassMark Storage dataset stores basic fields at top-level (type/size), not under specs if (-not $specs) { try { $stType = $null $stSize = $null try { $stType = ("" + $e.type).Trim() } catch { $stType = $null } try { $stSize = ("" + $e.size).Trim() } catch { $stSize = $null } if ($stType -or $stSize) { $specs = [pscustomobject]@{ Type = $stType; Size = $stSize } } } catch { } } return @{ Tier = $tierOut; Score = $scoreOut; Category = $catOut; EstimatedYear = $yrOut; Benchmarks = $bench; Sources = $sources; Specs = $specs } } } catch { } # Fallback: legacy speed/capacity string: "GB|Type|Speed" if ([string]::IsNullOrWhiteSpace($legacyStr)) { $legacyStr = ("" + $ComponentName) } if ($legacyStr -match "(\d+)\|(.*?)\|(\d+)") { $capacity = [int]$matches[1] $type = $matches[2] $speed = [int]$matches[3] # If the quick IO test under-reports (or fails), use the detected bus/media type # to avoid classifying NVMe as SATA SSD/HDD. This keeps tiers sane on Win10/11. if ($type -match "(?i)NVMe" -and $speed -lt 1500) { $speed = 2000 } elseif ($type -match "(?i)SSD" -and $speed -lt 450) { $speed = 500 } elseif ($type -match "(?i)HDD" -and $speed -lt 90) { $speed = 100 } if ($Database.storage -and $Database.storage.speed) { foreach ($tier in $Database.storage.speed) { if ($speed -ge $tier.minSpeed) { $result.Tier = $tier.tier $result.Score = $tier.score $result.Category = $tier.category break } } } } } "Display" { if ($ComponentName -match "(\d+)x(\d+)@(\d+)") { $width = [int]$matches[1] $hz = [int]$matches[3] $resScore = 50 $hzScore = 50 if ($Database.display) { if ($Database.display.resolution) { foreach ($tier in $Database.display.resolution) { if ($width -ge $tier.minWidth) { $resScore = $tier.score $result.Tier = $tier.tier break } } } if ($Database.display.refreshRate) { foreach ($tier in $Database.display.refreshRate) { if ($hz -ge $tier.minHz) { $hzScore = $tier.score break } } } } $result.Score = [math]::Round(($resScore * 0.7 + $hzScore * 0.3), 0) $result.Category = "$($matches[1])x$($matches[2]) @ $($matches[3])Hz" } } } return $result } function Get-UseCaseRecommendations { param([int]$PerformanceScore, [object]$Database) $recommendations = @{ Suitable = @() Limited = @() NotRecommended = @() } if (-not $Database -or -not $Database.useCases -or -not $Database.useCases.performance_ranges) { return $recommendations } foreach ($range in $Database.useCases.performance_ranges) { if ($PerformanceScore -ge $range.minScore -and $PerformanceScore -le $range.maxScore) { $recommendations.Suitable = $range.suitable $recommendations.Limited = $range.limited $recommendations.NotRecommended = $range.notRecommended $recommendations.Tier = $range.tier break } } return $recommendations } function Get-UpgradeRecommendations { param([hashtable]$CurrentSpecs, [object]$Database) $upgrades = @() if (-not $Database -or -not $Database.upgrades) { return $upgrades } if ($CurrentSpecs.RAM -le 4) { $upgrades += @{ Component = "RAM" Current = "$($CurrentSpecs.RAM)GB" Recommended = "16GB" Cost = "$50-80" Impact = "VERY HIGH" PerformanceGain = 60 Priority = 1 Description = "Critical upgrade - 4GB is insufficient for modern Windows." } } elseif ($CurrentSpecs.RAM -eq 8) { $upgrades += @{ Component = "RAM" Current = "8GB" Recommended = "16GB" Cost = "$30-50" Impact = "HIGH" PerformanceGain = 25 Priority = 2 Description = "Strongly recommended - 16GB is the standard for 2025." } } $storageType = (($CurrentSpecs.StorageType -as [string]) + "").Trim() $hasSSD = $false try { if ($CurrentSpecs -and $CurrentSpecs.ContainsKey('HasSSD')) { $hasSSD = [bool]$CurrentSpecs.HasSSD } } catch { $hasSSD = $false } # Only recommend buying an SSD if there isn't already an SSD/NVMe present. if ($storageType -match '(?i)\bHDD\b' -and -not $hasSSD) { $upgrades += @{ Component = "Storage" Current = "HDD" Recommended = "512GB SSD" Cost = "$35-60" Impact = "VERY HIGH" PerformanceGain = 200 Priority = 1 Description = "Most impactful upgrade! Boot time drops from minutes to seconds." } } if ($CurrentSpecs.BatteryHealth -gt 0 -and $CurrentSpecs.BatteryHealth -lt 50) { $upgrades += @{ Component = "Battery" Current = "$($CurrentSpecs.BatteryHealth)% Health" Recommended = "New Battery" Cost = "$40-120" Impact = "HIGH" PerformanceGain = 100 Priority = 2 Description = "Battery health below 50% means very poor runtime." } } if ($CurrentSpecs.CPUAge -gt 7 -or $CurrentSpecs.PerformanceScore -lt 40) { $upgrades += @{ Component = "Full Laptop" Current = "Current System" Recommended = "New Laptop" Cost = "$600-900" Impact = "VERY HIGH" PerformanceGain = 150 Priority = 1 Description = "System is significantly outdated. New mid-range laptop would provide substantially better performance." } } $upgrades = $upgrades | Sort-Object Priority return $upgrades } function Test-CPUBenchmark { try { $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $iterations = 0 while ($stopwatch.ElapsedMilliseconds -lt 3000) { $temp = [math]::Sqrt([math]::Pow((Get-Random -Maximum 10000), 2)) $temp = [math]::Log($temp + 1) $temp = [math]::Exp([math]::Log($temp + 1)) $iterations++ } $stopwatch.Stop() $opsPerSecond = [math]::Round($iterations / ($stopwatch.ElapsedMilliseconds / 1000), 0) $benchmarkScore = [math]::Min(100, [math]::Round(($opsPerSecond / 1000), 0)) return @{ OpsPerSecond = $opsPerSecond Score = $benchmarkScore } } catch { return @{ OpsPerSecond = 0; Score = 0 } } } function Get-CPUTemperatureReading { # Best-effort temperature reading across common Windows sensor sources. # Returns: @{ Celsius = ; Source = } $result = @{ Celsius = $null; Source = "N/A" } # Make sure WMI is running (many sensor paths depend on it) try { $null = Ensure-WmiServiceRunning } catch { } # 1) ACPI thermal zone (common on laptops; often missing on desktops) try { $thermal = Get-CimInstance -Namespace root\wmi -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction Stop | Select-Object -First 1 if ($thermal -and $thermal.CurrentTemperature) { $c = [math]::Round((([double]$thermal.CurrentTemperature / 10.0) - 273.15), 1) if ($c -gt -30 -and $c -lt 150) { $result.Celsius = $c $result.Source = "ACPI (MSAcpi_ThermalZoneTemperature)" return $result } } } catch { } # 2) Performance counter "Thermal Zone Information" (may report Kelvin) try { $ctr = Get-Counter "\\Thermal Zone Information(*)\\Temperature" -ErrorAction Stop $samples = @($ctr.CounterSamples) | Where-Object { $_ -and $_.CookedValue -ne $null } if ($samples.Count -gt 0) { $v = [double]($samples | Select-Object -First 1).CookedValue $c = if ($v -gt 200) { [math]::Round(($v - 273.15), 1) } else { [math]::Round($v, 1) } if ($c -gt -30 -and $c -lt 150) { $result.Celsius = $c $result.Source = "PerfCounter (Thermal Zone Information)" return $result } } } catch { } # 3) LibreHardwareMonitor WMI provider (if installed & running) try { $sensors = @(Get-CimInstance -Namespace root\LibreHardwareMonitor -ClassName Sensor -ErrorAction Stop | Where-Object { $_.SensorType -eq "Temperature" -and $_.Value -ne $null }) if ($sensors.Count -gt 0) { $preferred = $sensors | Where-Object { ($_.Name -match "(?i)CPU\s*Package|Package|Tdie|CPU\s*Core") -or ($_.Identifier -match "(?i)/cpu/") } $best = @($preferred)[0] if (-not $best) { $cpuLike = $sensors | Where-Object { $_.Name -match "(?i)CPU" -or $_.Identifier -match "(?i)/cpu/" } $best = $cpuLike | Sort-Object -Property Value -Descending | Select-Object -First 1 } if (-not $best) { $best = $sensors | Sort-Object -Property Value -Descending | Select-Object -First 1 } $c = [math]::Round([double]$best.Value, 1) if ($c -gt -30 -and $c -lt 150) { $result.Celsius = $c $result.Source = "LibreHardwareMonitor WMI ($($best.Name))" return $result } } } catch { } # 4) OpenHardwareMonitor WMI provider (if installed & running) try { $sensors = @(Get-CimInstance -Namespace root\OpenHardwareMonitor -ClassName Sensor -ErrorAction Stop | Where-Object { $_.SensorType -eq "Temperature" -and $_.Value -ne $null }) if ($sensors.Count -gt 0) { $preferred = $sensors | Where-Object { ($_.Name -match "(?i)CPU\s*Package|Package|Tdie|CPU\s*Core") -or ($_.Identifier -match "(?i)/cpu/") } $best = @($preferred)[0] if (-not $best) { $cpuLike = $sensors | Where-Object { $_.Name -match "(?i)CPU" -or $_.Identifier -match "(?i)/cpu/" } $best = $cpuLike | Sort-Object -Property Value -Descending | Select-Object -First 1 } if (-not $best) { $best = $sensors | Sort-Object -Property Value -Descending | Select-Object -First 1 } $c = [math]::Round([double]$best.Value, 1) if ($c -gt -30 -and $c -lt 150) { $result.Celsius = $c $result.Source = "OpenHardwareMonitor WMI ($($best.Name))" return $result } } } catch { } return $result } function Test-CPUTemperature { $tempBefore = 0 $tempAfter = 0 $tempMin = 999 $tempMax = 0 $tempAvg = 0 $tempDelta = 0 $temperatures = @() try { # Get temperature before stress (best-effort) $before = Get-CPUTemperatureReading if (-not $before -or $null -eq $before.Celsius) { return @{ Before = 0 After = 0 Min = 0 Max = 0 Avg = 0 Delta = 0 Tested = $false HasHeatingIssues = $false HeatingAssessment = "Unable to Test" HeatingIssues = @() CoolingRecommendations = @("Temperature sensor not available") } } $tempBefore = [double]$before.Celsius $tempMin = $tempBefore $tempMax = $tempBefore # Run stress test for 15 seconds and collect temperatures $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $lastCheck = 0 while ($stopwatch.ElapsedMilliseconds -lt 5000) { # Intensive calculation $null = [math]::Sqrt([math]::Pow((Get-Random -Maximum 100000), 2)) $null = [math]::Log([math]::Exp((Get-Random -Maximum 100) + 1)) # Check temperature every 2 seconds if ($stopwatch.ElapsedMilliseconds - $lastCheck -ge 2000) { $r = Get-CPUTemperatureReading if ($r -and $null -ne $r.Celsius) { $currentTemp = [double]$r.Celsius $temperatures += $currentTemp if ($currentTemp -lt $tempMin) { $tempMin = $currentTemp } if ($currentTemp -gt $tempMax) { $tempMax = $currentTemp } } $lastCheck = $stopwatch.ElapsedMilliseconds } } $stopwatch.Stop() Start-Sleep -Seconds 2 # Let temp stabilize # Get temperature after stress $after = Get-CPUTemperatureReading if ($after -and $null -ne $after.Celsius) { $tempAfter = [double]$after.Celsius } else { $tempAfter = $tempBefore } # Calculate average if ($temperatures.Count -gt 0) { $tempAvg = [math]::Round(($temperatures | Measure-Object -Average).Average, 1) } $tempDelta = [math]::Round($tempAfter - $tempBefore, 1) # Assess heating issues $heatingIssues = @() $coolingRecommendations = @() $heatingAssessment = "No Issues" $hasHeatingIssues = $false # Critical issues (temperature > 95°C) if ($tempMax -gt 95) { $hasHeatingIssues = $true $heatingAssessment = "Critical Issues" $heatingIssues += "Maximum temperature reached $tempMax°C (Critical - above 95°C)" $heatingIssues += "CPU may throttle performance to prevent damage" $coolingRecommendations += "[URGENT] Clean laptop vents and fans immediately" $coolingRecommendations += "[URGENT] Consider professional thermal paste replacement" $coolingRecommendations += "[URGENT] Use laptop on hard, flat surface for better airflow" } # Moderate issues (temperature > 85°C or avg > 75°C) elseif ($tempMax -gt 85 -or $tempAvg -gt 75) { $hasHeatingIssues = $true $heatingAssessment = "Moderate Issues" if ($tempMax -gt 85) { $heatingIssues += "Maximum temperature reached $tempMax°C (High - above 85°C)" } if ($tempAvg -gt 75) { $heatingIssues += "Average temperature during stress: $tempAvg°C (High - above 75°C)" } $heatingIssues += "Cooling system may be insufficient or blocked" $coolingRecommendations += "Clean laptop vents and fans" $coolingRecommendations += "Ensure laptop is on hard, flat surface" $coolingRecommendations += "Consider using laptop cooling pad" $coolingRecommendations += "Check if thermal paste needs replacement" } # Minor issues (rapid temperature rise or moderately high temps) elseif ($tempDelta -gt 25 -or ($tempDelta -gt 20 -and $tempMax -gt 80)) { $hasHeatingIssues = $true $heatingAssessment = "Minor Issues" if ($tempDelta -gt 25) { $heatingIssues += "Rapid temperature increase: $tempDelta°C in 15 seconds" } if ($tempMax -gt 80) { $heatingIssues += "Maximum temperature reached $tempMax°C (Slightly high)" } $coolingRecommendations += "Monitor temperatures during heavy use" $coolingRecommendations += "Consider cleaning laptop vents" $coolingRecommendations += "Ensure good airflow around laptop" } # Good cooling performance elseif ($tempMax -lt 75 -and $tempDelta -lt 20) { $heatingAssessment = "Excellent" $coolingRecommendations += "Cooling system is performing well" $coolingRecommendations += "Temperature stayed within safe limits" $coolingRecommendations += "Continue regular maintenance to keep performance optimal" } # Acceptable performance else { $heatingAssessment = "Good" $coolingRecommendations += "Cooling system is functioning adequately" $coolingRecommendations += "Regular maintenance recommended to maintain performance" } return @{ Before = $tempBefore After = $tempAfter Min = $tempMin Max = $tempMax Avg = $tempAvg Delta = $tempDelta Tested = $true HasHeatingIssues = $hasHeatingIssues HeatingAssessment = $heatingAssessment HeatingIssues = $heatingIssues CoolingRecommendations = $coolingRecommendations } } catch { return @{ Before = 0 After = 0 Min = 0 Max = 0 Avg = 0 Delta = 0 Tested = $false HasHeatingIssues = $false HeatingAssessment = "Unable to Test" HeatingIssues = @() CoolingRecommendations = @("Temperature sensor not available") } } } #endregion #region Battery Detection (keeping existing function) function Get-BatteryInfo { $batteryInfo = @{ Detected = $false Health = 0 CurrentCharge = 0 DesignCapacity = 0 FullChargeCapacity = 0 Debug = @() # non-verbose diagnostics (also available via -Verbose) Status = "Not Detected" BackupTime = "N/A" CycleCount = "N/A" Manufacturer = "N/A" Chemistry = "N/A" Voltage = "N/A" CurrentRate = "N/A" RemainingCapacity = "N/A" } # Helper: normalize WMI values (handles arrays/multiple batteries) function _Get-IntSum($values) { $vals = @($values) | Where-Object { $_ -ne $null } if (-not $vals -or $vals.Count -eq 0) { return 0 } $nums = @() foreach ($v in $vals) { try { if ($v -is [string]) { $digits = ($v -replace '[^0-9]', '') if ($digits) { $nums += [int]$digits } } else { $nums += [int]$v } } catch { # ignore } } if (-not $nums -or $nums.Count -eq 0) { return 0 } return [int](($nums | Measure-Object -Sum).Sum) } $batteries = @(Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue) if ($batteries -and $batteries.Count -gt 0) { $batteryInfo.Detected = $true $b0 = $batteries | Select-Object -First 1 $batteryInfo.CurrentCharge = $b0.EstimatedChargeRemaining $batteryInfo.Chemistry = switch ($b0.Chemistry) { 1 { "Other" }; 2 { "Unknown" }; 3 { "Lead Acid" } 4 { "Nickel Cadmium" }; 5 { "Nickel Metal Hydride" } 6 { "Lithium Ion" }; 7 { "Zinc Air" } 8 { "Lithium Polymer" } default { "Code $($b0.Chemistry)" } } $batteryInfo.Status = switch ($b0.BatteryStatus) { 1 { "Discharging" }; 2 { "On AC Power" }; 3 { "Fully Charged" } 4 { "Low" }; 5 { "Critical" }; 6 { "Charging" } 7 { "Charging and High" }; 8 { "Charging and Low" } 9 { "Charging and Critical" }; 10 { "Undefined" } 11 { "Partially Charged" } default { "Unknown" } } # Some systems populate capacities here; normalize if present (and don't clobber later sources) $winDesign = _Get-IntSum (@($batteries | ForEach-Object { $_.DesignCapacity })) if ($batteryInfo.DesignCapacity -le 0 -and $winDesign -gt 0) { $batteryInfo.DesignCapacity = $winDesign $batteryInfo.Debug += "DesignCapacity from Win32_Battery = $winDesign (units vary by vendor)" } $winFull = _Get-IntSum (@($batteries | ForEach-Object { $_.FullChargeCapacity })) if ($batteryInfo.FullChargeCapacity -le 0 -and $winFull -gt 0) { $batteryInfo.FullChargeCapacity = $winFull $batteryInfo.Debug += "FullChargeCapacity from Win32_Battery = $winFull (units vary by vendor)" } } try { $battStatus = @(Get-CimInstance -Namespace root\wmi -ClassName BatteryFullChargedCapacity -ErrorAction Stop) if ($battStatus -and $battStatus.Count -gt 0) { $fullVals = @($battStatus | ForEach-Object { $_.FullChargedCapacity }) $sumFull = _Get-IntSum $fullVals if ($sumFull -gt 0) { if ($batteryInfo.FullChargeCapacity -le 0 -or $sumFull -gt $batteryInfo.FullChargeCapacity) { $batteryInfo.FullChargeCapacity = $sumFull } $batteryInfo.Debug += "FullChargeCapacity from WMI(root\\wmi:BatteryFullChargedCapacity) = $sumFull mWh" Write-Verbose "[Battery] FullChargeCapacity (WMI BatteryFullChargedCapacity) = $sumFull mWh" } else { $batteryInfo.Debug += "WMI BatteryFullChargedCapacity returned 0/empty" } } } catch { $batteryInfo.Debug += "WMI BatteryFullChargedCapacity query failed: $($_.Exception.Message)" } try { $battStatusDetailed = @(Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus -ErrorAction Stop) if ($battStatusDetailed -and $battStatusDetailed.Count -gt 0) { $bs = $battStatusDetailed | Select-Object -First 1 if ($bs.Voltage) { $batteryInfo.Voltage = "$([math]::Round($bs.Voltage / 1000, 1)) V" } if ($bs.RemainingCapacity) { $batteryInfo.RemainingCapacity = "$([math]::Round($bs.RemainingCapacity / 1000, 1)) Wh" } if ($bs.ChargeRate) { $watts = [math]::Abs([math]::Round($bs.ChargeRate / 1000, 1)) $batteryInfo.CurrentRate = "$watts W" } } } catch { $batteryInfo.Debug += "WMI BatteryStatus query failed: $($_.Exception.Message)" } try { $battStatic = @(Get-CimInstance -Namespace root\wmi -ClassName BatteryStaticData -ErrorAction Stop) if ($battStatic -and $battStatic.Count -gt 0) { $designVals = @($battStatic | ForEach-Object { $_.DesignedCapacity }) $sumDesign = _Get-IntSum $designVals if ($sumDesign -gt 0) { if ($batteryInfo.DesignCapacity -le 0 -or $sumDesign -gt $batteryInfo.DesignCapacity) { $batteryInfo.DesignCapacity = $sumDesign } $batteryInfo.Debug += "DesignCapacity from WMI(root\\wmi:BatteryStaticData) = $sumDesign mWh" Write-Verbose "[Battery] DesignCapacity (WMI BatteryStaticData) = $sumDesign mWh" } else { $batteryInfo.Debug += "WMI BatteryStaticData returned 0/empty DesignedCapacity" } # Manufacturer name (take first non-empty) foreach ($bs in $battStatic) { if ($bs.ManufactureName) { try { $m = [System.Text.Encoding]::Unicode.GetString($bs.ManufactureName).Trim([char]0) if ($m) { $batteryInfo.Manufacturer = $m; break } } catch {} } } } } catch { $batteryInfo.Debug += "WMI BatteryStaticData query failed: $($_.Exception.Message)" } # Only run powercfg fallback if we still need capacities or cycle count if (($batteryInfo.DesignCapacity -le 0) -or ($batteryInfo.FullChargeCapacity -le 0) -or ($batteryInfo.CycleCount -eq "N/A")) { try { $tempReport = Join-Path $env:TEMP "battery-report-temp.html" $pcfgOut = (powercfg /batteryreport /output $tempReport /duration 1 2>&1 | Out-String) if (Test-Path $tempReport) { $reportContent = Get-Content $tempReport -Raw # Strip tags to make matching robust across minor HTML changes and locales with separators $reportText = ($reportContent -replace '<[^>]+>', ' ') $reportText = ($reportText -replace '\s+', ' ').Trim() if ($reportText -match 'DESIGN\s+CAPACITY\s+([0-9][0-9,\.\s]+)\s*mWh') { $digits = ($matches[1] -replace '[^0-9]', '') if ($digits) { if ($batteryInfo.DesignCapacity -le 0) { $batteryInfo.DesignCapacity = [int]$digits } $batteryInfo.Debug += "DesignCapacity from powercfg batteryreport = $($batteryInfo.DesignCapacity) mWh" Write-Verbose "[Battery] DesignCapacity (powercfg) = $($batteryInfo.DesignCapacity) mWh" } } else { $batteryInfo.Debug += "powercfg batteryreport did not match DESIGN CAPACITY" } if ($reportText -match 'FULL\s+CHARGE\s+CAPACITY\s+([0-9][0-9,\.\s]+)\s*mWh') { $digits = ($matches[1] -replace '[^0-9]', '') if ($digits) { if ($batteryInfo.FullChargeCapacity -le 0) { $batteryInfo.FullChargeCapacity = [int]$digits } $batteryInfo.Debug += "FullChargeCapacity from powercfg batteryreport = $($batteryInfo.FullChargeCapacity) mWh" Write-Verbose "[Battery] FullChargeCapacity (powercfg) = $($batteryInfo.FullChargeCapacity) mWh" } } else { $batteryInfo.Debug += "powercfg batteryreport did not match FULL CHARGE CAPACITY" } if ($reportText -match 'CYCLE\s+COUNT\s+(\d+)') { $batteryInfo.CycleCount = $matches[1] } Remove-Item $tempReport -Force -ErrorAction SilentlyContinue } else { if ($pcfgOut) { $batteryInfo.Debug += "powercfg batteryreport failed or did not write file: $($pcfgOut.Trim())" } else { $batteryInfo.Debug += "powercfg batteryreport did not create output file" } } } catch { $batteryInfo.Debug += "powercfg batteryreport parsing failed: $($_.Exception.Message)" } } else { $batteryInfo.Debug += "powercfg batteryreport skipped (capacities already detected)" } if ($batteryInfo.FullChargeCapacity -gt 0 -and $batteryInfo.DesignCapacity -gt 0) { $batteryInfo.Health = [math]::Round(($batteryInfo.FullChargeCapacity / $batteryInfo.DesignCapacity) * 100, 1) if ($batteryInfo.Health -gt 100) { $batteryInfo.Health = 100 } $batteryInfo.Debug += "Battery Health computed = $($batteryInfo.Health)%" Write-Verbose "[Battery] Health = $($batteryInfo.Health)% (Full=$($batteryInfo.FullChargeCapacity), Design=$($batteryInfo.DesignCapacity))" } elseif ($batteryInfo.Detected) { $batteryInfo.Debug += "Battery detected but capacities missing/0 (Full=$($batteryInfo.FullChargeCapacity), Design=$($batteryInfo.DesignCapacity))" } return $batteryInfo } function Get-ChargerInfo { $chargerInfo = @{ Detected = $false Manufacturer = "N/A" Voltage = "N/A" Current = "N/A" Wattage = "N/A" Status = "Not Detected" } try { $battery = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue if ($battery) { $batteryStatus = $battery.BatteryStatus if ($batteryStatus -eq 2 -or $batteryStatus -eq 6 -or $batteryStatus -eq 7 -or $batteryStatus -eq 3) { $chargerInfo.Detected = $true $chargerInfo.Status = "Connected" } else { $chargerInfo.Status = "Not Connected" } } $portableBattery = Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus -ErrorAction SilentlyContinue if ($portableBattery -and $portableBattery.ChargeRate -gt 0) { $chargerInfo.Detected = $true $watts = [math]::Round($portableBattery.ChargeRate / 1000, 1) $chargerInfo.Wattage = "$watts W" if ($portableBattery.Voltage) { $volts = [math]::Round($portableBattery.Voltage / 1000, 1) $chargerInfo.Voltage = "$volts V" if ($volts -gt 0) { $amps = [math]::Round($watts / $volts, 2) $chargerInfo.Current = "$amps A" } } } } catch {} return $chargerInfo } function Test-AdapterHealth { $adapterTest = @{ Connected = $false Voltage = @{ Current = 0 Min = 0 Max = 0 Avg = 0 Stability = "Unknown" } Current = @{ Current = 0 Min = 0 Max = 0 Avg = 0 } Power = @{ Current = 0 Min = 0 Max = 0 Avg = 0 EstimatedRating = "Unknown" } ChargingRate = @{ Current = 0 Min = 0 Max = 0 Avg = 0 IsCharging = $false } BatteryLevel = @{ Start = 0 End = 0 Delta = 0 } Health = "Unknown" Issues = @() Recommendations = @() Tested = $false } try { # Check if adapter is connected $battery = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue if ($battery) { $batteryStatus = $battery.BatteryStatus $adapterTest.Connected = ($batteryStatus -eq 2 -or $batteryStatus -eq 6 -or $batteryStatus -eq 7 -or $batteryStatus -eq 3) if (-not $adapterTest.Connected) { $adapterTest.Health = "Not Connected" $adapterTest.Recommendations += "Connect AC adapter to perform comprehensive testing" return $adapterTest } # Initialize tracking arrays $voltages = @() $currents = @() $powers = @() $chargeRates = @() # Get initial battery level $batteryStart = Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus -ErrorAction SilentlyContinue | Select-Object -First 1 if ($batteryStart -and $batteryStart.RemainingCapacity -and $batteryStart.MaxCapacity) { $adapterTest.BatteryLevel.Start = [math]::Round((([double]$batteryStart.RemainingCapacity / [double]$batteryStart.MaxCapacity) * 100), 1) } # Monitor for 10 seconds $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $lastCheck = 0 while ($stopwatch.ElapsedMilliseconds -lt 10000) { # Sample every second if ($stopwatch.ElapsedMilliseconds - $lastCheck -ge 1000) { $portableBattery = Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus -ErrorAction SilentlyContinue | Select-Object -First 1 if ($portableBattery) { # Voltage if ($portableBattery.Voltage -and $portableBattery.Voltage -gt 0) { $currentVolts = [math]::Round(([double]$portableBattery.Voltage / 1000.0), 2) if ($currentVolts -gt 0) { $voltages += $currentVolts } } # Charge rate (positive = charging, negative = discharging) if ($portableBattery.ChargeRate) { $currentWatts = [math]::Round(([double]$portableBattery.ChargeRate / 1000.0), 2) $powers += $currentWatts if ($currentWatts -gt 0) { $chargeRates += $currentWatts $adapterTest.ChargingRate.IsCharging = $true } # Calculate current if we have voltage and power if ($voltages.Count -gt 0 -and $currentWatts -gt 0) { $lastVolts = $voltages[-1] if ($lastVolts -gt 0) { $currentAmps = [math]::Round(($currentWatts / $lastVolts), 2) if ($currentAmps -gt 0) { $currents += $currentAmps } } } } } $lastCheck = $stopwatch.ElapsedMilliseconds } } $stopwatch.Stop() # Get final battery level $batteryEnd = Get-CimInstance -Namespace root\wmi -ClassName BatteryStatus -ErrorAction SilentlyContinue | Select-Object -First 1 if ($batteryEnd -and $batteryEnd.RemainingCapacity -and $batteryEnd.MaxCapacity) { $adapterTest.BatteryLevel.End = [math]::Round((([double]$batteryEnd.RemainingCapacity / [double]$batteryEnd.MaxCapacity) * 100), 1) $adapterTest.BatteryLevel.Delta = [math]::Round(($adapterTest.BatteryLevel.End - $adapterTest.BatteryLevel.Start), 2) } # Calculate statistics if ($voltages.Count -gt 0) { $adapterTest.Voltage.Current = $voltages[-1] $adapterTest.Voltage.Min = [math]::Round(($voltages | Measure-Object -Minimum).Minimum, 2) $adapterTest.Voltage.Max = [math]::Round(($voltages | Measure-Object -Maximum).Maximum, 2) $adapterTest.Voltage.Avg = [math]::Round(($voltages | Measure-Object -Average).Average, 2) # Check voltage stability (should be within 5% of average) $voltageVariance = $adapterTest.Voltage.Max - $adapterTest.Voltage.Min $voltageVariancePercent = [math]::Round(($voltageVariance / $adapterTest.Voltage.Avg) * 100, 1) if ($voltageVariancePercent -lt 3) { $adapterTest.Voltage.Stability = "Excellent" } elseif ($voltageVariancePercent -lt 5) { $adapterTest.Voltage.Stability = "Good" } elseif ($voltageVariancePercent -lt 10) { $adapterTest.Voltage.Stability = "Fair" $adapterTest.Issues += "Voltage fluctuation detected ($voltageVariancePercent percent variance)" } else { $adapterTest.Voltage.Stability = "Poor" $adapterTest.Issues += "High voltage instability ($voltageVariancePercent percent variance)" } } if ($currents.Count -gt 0) { $adapterTest.Current.Current = $currents[-1] $adapterTest.Current.Min = [math]::Round(($currents | Measure-Object -Minimum).Minimum, 2) $adapterTest.Current.Max = [math]::Round(($currents | Measure-Object -Maximum).Maximum, 2) $adapterTest.Current.Avg = [math]::Round(($currents | Measure-Object -Average).Average, 2) } if ($powers.Count -gt 0) { $adapterTest.Power.Current = $powers[-1] $adapterTest.Power.Min = [math]::Round(($powers | Measure-Object -Minimum).Minimum, 2) $adapterTest.Power.Max = [math]::Round(($powers | Measure-Object -Maximum).Maximum, 2) $adapterTest.Power.Avg = [math]::Round(($powers | Measure-Object -Average).Average, 2) # Estimate adapter rating (round up to common wattages) $avgPower = $adapterTest.Power.Avg if ($avgPower -gt 0) { $commonWattages = @(45, 65, 90, 120, 135, 150, 180, 230, 240, 330) $estimatedRating = 65 # default foreach ($wattage in $commonWattages) { if ($avgPower -le ($wattage * 0.85)) { # Adapters typically deliver 85% under load $estimatedRating = $wattage break } } $adapterTest.Power.EstimatedRating = "$estimatedRating W" } } if ($chargeRates.Count -gt 0) { $adapterTest.ChargingRate.Current = $chargeRates[-1] $adapterTest.ChargingRate.Min = [math]::Round(($chargeRates | Measure-Object -Minimum).Minimum, 2) $adapterTest.ChargingRate.Max = [math]::Round(($chargeRates | Measure-Object -Maximum).Maximum, 2) $adapterTest.ChargingRate.Avg = [math]::Round(($chargeRates | Measure-Object -Average).Average, 2) } # Determine overall health $healthScore = 100 # Check charging functionality (but allow for 100% battery) $currentBatteryLevel = $adapterTest.BatteryLevel.Start if (-not $adapterTest.ChargingRate.IsCharging) { # Only flag as issue if battery is not at 100% if ($currentBatteryLevel -lt 99) { $healthScore -= 50 $adapterTest.Issues += "Adapter connected but battery not charging (Battery at $currentBatteryLevel percent)" $adapterTest.Recommendations += "Check adapter cable and port for damage" $adapterTest.Recommendations += "Try a different power outlet" } else { $adapterTest.Recommendations += "Battery is fully charged ($currentBatteryLevel percent) - not charging is normal" } } # Check voltage stability if ($adapterTest.Voltage.Stability -eq "Poor") { $healthScore -= 30 $adapterTest.Recommendations += "Adapter may be failing - consider replacement" } elseif ($adapterTest.Voltage.Stability -eq "Fair") { $healthScore -= 15 $adapterTest.Recommendations += "Monitor adapter performance - slight instability detected" } # Check charging rate if ($adapterTest.ChargingRate.Avg -gt 0 -and $adapterTest.ChargingRate.Avg -lt 15) { $healthScore -= 20 $adapterTest.Issues += "Low charging rate detected ($(adapterTest.ChargingRate.Avg)W)" $adapterTest.Recommendations += "Adapter may be underpowered or degraded" } # Assign health rating if ($healthScore -ge 85) { $adapterTest.Health = "Excellent" } elseif ($healthScore -ge 70) { $adapterTest.Health = "Good" } elseif ($healthScore -ge 50) { $adapterTest.Health = "Fair" } else { $adapterTest.Health = "Poor" } # Add generic recommendations if everything is good if ($adapterTest.Issues.Count -eq 0) { $adapterTest.Recommendations += "Adapter is functioning properly" $adapterTest.Recommendations += "Keep adapter cable organized to prevent damage" $adapterTest.Recommendations += "Avoid exposing adapter to extreme temperatures" } # Add notes about measurements $adapterTest.Notes = @() $adapterTest.Notes += "Battery Voltage: $($adapterTest.Voltage.Avg)V (voltage measured at battery terminals)" $adapterTest.Notes += "Typical laptop adapter output: 19-20V DC (cannot be directly measured from system)" if ($adapterTest.Power.EstimatedRating -ne "Unknown") { $adapterTest.Notes += "Estimated Adapter Power Rating: $($adapterTest.Power.EstimatedRating)" } if ($adapterTest.Current.Avg -gt 0) { $adapterTest.Notes += "Charging Current: $($adapterTest.Current.Avg)A (calculated from power and voltage)" } $adapterTest.Tested = $true } else { $adapterTest.Health = "Unable to Test" } } catch { $adapterTest.Health = "Test Failed" $adapterTest.Issues += "Error during testing: $($_.Exception.Message)" } return $adapterTest } function Get-DesktopPowerInfo { # Note: Windows generally cannot read internal PSU wattage/health via WMI on consumer/workstation desktops. $info = @{ Type = "Desktop" ActivePowerPlan = "Unknown" ActivePowerPlanGuid = "N/A" ActivePowerPlanSource = "N/A" UPSDetected = $false UPSName = "N/A" PsuWattage = "N/A" PsuWattageSource = "N/A" PowerDrawWatts = "N/A" PowerDrawSource = "N/A" CpuPackagePowerWatts = "N/A" GpuPowerWatts = "N/A" SensorProvider = "None" FanReadings = @() Notes = @( "Desktop PSUs typically do not expose health/wattage to Windows via standard APIs." ) Recommendations = @( "If you suspect power issues: check PSU wattage/brand label, inspect cables, and monitor stability under load." ) Tested = $true } # Ensure WMI is available (best-effort) try { $null = Ensure-WmiServiceRunning } catch { } # Active power plan try { $plan = Get-CimInstance -Namespace root\cimv2\power -ClassName Win32_PowerPlan -Filter "IsActive=true" -ErrorAction Stop if ($plan) { $info.ActivePowerPlan = $plan.ElementName $info.ActivePowerPlanGuid = ($plan.InstanceID -replace '.*\\{','{' -replace '\\}.*','}') $info.ActivePowerPlanSource = "Win32_PowerPlan" } } catch { try { $out = (powercfg /getactivescheme 2>$null) | Out-String if ($out -match "GUID:\s*([0-9a-fA-F-]{36})\s*\(([^\)]+)\)") { $info.ActivePowerPlanGuid = $matches[1] $info.ActivePowerPlan = $matches[2] $info.ActivePowerPlanSource = "powercfg" } } catch { } } # UPS (best-effort): some UPS devices can appear as Win32_Battery. try { $batt = Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue if ($batt) { $info.UPSDetected = $true $info.UPSName = ($batt | Select-Object -First 1).Name $info.Notes += "A battery-like power device was detected (could be a UPS)." } } catch { } # PSU wattage (RATED) via WMI (rarely populated on desktops, but try anyway) try { $psus = @(Get-CimInstance -ClassName Win32_PowerSupply -ErrorAction Stop) if ($psus.Count -gt 0) { $psu = $psus | Select-Object -First 1 $propsToTry = @("TotalOutputPower","MaxOutputWattage","PowerCapacity") foreach ($p in $propsToTry) { try { if ($psu.PSObject.Properties.Name -contains $p) { $val = [double]$psu.$p if ($val -gt 0 -and $val -lt 5000) { $info.PsuWattage = "$([math]::Round($val,0)) W" $info.PsuWattageSource = "Win32_PowerSupply.$p" $info.Notes += "PSU wattage was obtained from WMI (uncommon; verify against PSU label)." break } } } catch { } } } } catch { } # PSU wattage (RATED) via CIM (sometimes exists even when Win32_PowerSupply is empty) if ($info.PsuWattage -eq 'N/A') { try { $psus2 = @(Get-CimInstance -Namespace root\cimv2 -ClassName CIM_PowerSupply -ErrorAction Stop) if ($psus2.Count -gt 0) { $psu2 = $psus2 | Select-Object -First 1 $propsToTry2 = @('RatedOutputPower','NameplatePower','TotalOutputPower','MaxOutputWattage','PowerCapacity') foreach ($p2 in $propsToTry2) { try { if ($psu2.PSObject.Properties.Name -contains $p2) { $val2 = [double]$psu2.$p2 if ($val2 -gt 0 -and $val2 -lt 5000) { $info.PsuWattage = "$([math]::Round($val2,0)) W" $info.PsuWattageSource = "CIM_PowerSupply.$p2" $info.Notes += "PSU wattage was obtained from CIM (uncommon; verify against PSU label)." break } } } catch { } } } } catch { } } # Sensor provider (best-effort): LibreHardwareMonitor / OpenHardwareMonitor WMI namespaces try { $providers = @( @{ Namespace = 'root\LibreHardwareMonitor'; Name = 'LibreHardwareMonitor WMI' }, @{ Namespace = 'root\OpenHardwareMonitor'; Name = 'OpenHardwareMonitor WMI' } ) foreach ($prov in $providers) { $sensors = $null try { $sensors = @(Get-CimInstance -Namespace $prov.Namespace -ClassName Sensor -ErrorAction Stop) } catch { $sensors = $null } if (-not $sensors -or $sensors.Count -eq 0) { continue } $info.SensorProvider = $prov.Name # Fan readings try { $fans = @($sensors | Where-Object { $_.SensorType -eq 'Fan' -and $_.Value -ne $null } | Sort-Object -Property Value -Descending) if ($fans.Count -gt 0) { $info.FanReadings = @($fans | Select-Object -First 6 | ForEach-Object { "$($_.Name): $([math]::Round([double]$_.Value,0)) RPM" }) } } catch { } # Power readings try { $power = @($sensors | Where-Object { $_.SensorType -eq 'Power' -and $_.Value -ne $null }) if ($power.Count -gt 0) { $systemLike = $power | Where-Object { $_.Name -match '(?i)Total\s*System|System\s*Power|Total\s*Power|PSU|Input\s*Power' -or $_.Identifier -match '(?i)/psu/' } $bestSystem = @($systemLike | Sort-Object -Property Value -Descending)[0] if ($bestSystem -and $bestSystem.Value -ne $null) { $w = [math]::Round([double]$bestSystem.Value, 0) $info.PowerDrawWatts = "$w W" $info.PowerDrawSource = "$($prov.Name) ($($bestSystem.Name))" } $cpuLike = $power | Where-Object { ($_.Name -match '(?i)CPU' -or $_.Identifier -match '(?i)/cpu/') -and $_.Name -match '(?i)Package|CPU' } $bestCpu = @($cpuLike | Sort-Object -Property Value -Descending)[0] if ($bestCpu -and $bestCpu.Value -ne $null) { $info.CpuPackagePowerWatts = "$([math]::Round([double]$bestCpu.Value,0)) W" } $gpuLike = $power | Where-Object { ($_.Name -match '(?i)GPU' -or $_.Identifier -match '(?i)/gpu/') } $bestGpu = @($gpuLike | Sort-Object -Property Value -Descending)[0] if ($bestGpu -and $bestGpu.Value -ne $null) { $info.GpuPowerWatts = "$([math]::Round([double]$bestGpu.Value,0)) W" } if ($info.PowerDrawWatts -eq 'N/A' -and ($bestCpu -or $bestGpu)) { $sum = 0.0 if ($bestCpu -and $bestCpu.Value -ne $null) { $sum += [double]$bestCpu.Value } if ($bestGpu -and $bestGpu.Value -ne $null) { $sum += [double]$bestGpu.Value } if ($sum -gt 0) { $info.PowerDrawWatts = "$([math]::Round($sum,0)) W" $info.PowerDrawSource = "$($prov.Name) (CPU+GPU sum)" $info.Notes += "Power draw is estimated from available sensors; this is not the PSU's rated wattage." } } } } catch { } # If a provider is available, stop at the first successful provider break } } catch { } if ($info.SensorProvider -eq 'None') { $info.Notes += "For real-time fan/power sensors, run LibreHardwareMonitor or OpenHardwareMonitor with WMI enabled." $info.Recommendations += "Optional: Install/run LibreHardwareMonitor (enable WMI) to expose fan speeds and power draw to this report." } return $info } function Get-TemperatureInfo { $tempInfo = @{ CPUTemp = "N/A" Status = "Unknown" Warning = $false CelsiusValue = 0 Source = "N/A" Notes = @() StressTest = @{ Before = 0 After = 0 Delta = 0 Tested = $false } } $reading = Get-CPUTemperatureReading if ($reading -and $null -ne $reading.Celsius) { $celsius = [double]$reading.Celsius $tempInfo.Source = [string]$reading.Source $tempInfo.CelsiusValue = $celsius $tempInfo.CPUTemp = "$celsius C" if ($celsius -lt 0) { $tempInfo.Status = "Sensor Error" } elseif ($celsius -lt 50) { $tempInfo.Status = "Excellent - Cool" } elseif ($celsius -lt 65) { $tempInfo.Status = "Good - Normal" } elseif ($celsius -lt 75) { $tempInfo.Status = "Warm - Acceptable" } elseif ($celsius -lt 85) { $tempInfo.Status = "Hot - Consider Cleaning" $tempInfo.Warning = $true } else { $tempInfo.Status = "Critical - Cooling Issue!" $tempInfo.Warning = $true } # Run stress test only when a sensor is readable $stressResult = Test-CPUTemperature $tempInfo.StressTest = $stressResult } else { $tempInfo.CPUTemp = "N/A" $tempInfo.Status = "Sensor Not Found" $tempInfo.Source = "No readable temperature sensors (ACPI/PerfCounter/LHM/OHM)" $tempInfo.Notes += "Many desktops/workstations do not expose CPU temperature to Windows via standard APIs." $tempInfo.Notes += "If you install/run LibreHardwareMonitor or OpenHardwareMonitor with WMI enabled, this tool can read temperatures from their WMI namespaces." } return $tempInfo } function Get-SanitizedSerial { param([string]$Serial) if ([string]::IsNullOrWhiteSpace($Serial) -or $Serial -match '(?i)^(To Be Filled|O\.?E\.?M\.?|Default string|Not Specified|None|System Serial|0+|N/?A|Unknown|Chassis Serial|Empty|^\.+$)') { return "Not Available" } $cleaned = $Serial.Trim() # Try to decode hex-encoded serial numbers (common on NVMe/SATA drives via WMI) $decoded = Convert-HexSerial $cleaned if ($decoded) { return $decoded } # If hex-looking with underscores/separators AND contains hex letters, reformat to clean groups of 4 if ($cleaned -match '[_\-]' -and $cleaned -match '^[0-9A-Fa-f_\-\s]+$' -and ($cleaned -replace '[_\-\s]','').Length -ge 8) { $hexClean = ($cleaned -replace '[_\-\s]','').ToUpper() if ($hexClean -match '^[0-9A-F]+$' -and $hexClean -match '[A-F]') { # Strip trailing zeros $hexClean = $hexClean -replace '0+$', '' if ($hexClean.Length -ge 4) { return ($hexClean -replace '(.{4})', '$1 ').Trim() } } } return $cleaned } function Convert-HexSerial { param([string]$Raw) # Detect hex-encoded serials: patterns like "3455_4630_4EC0..." or "5330325A4E454A43..." # WMI often returns NVMe/SATA serials as hex-encoded ASCII strings $hex = $Raw -replace '[_\s\-]', '' # Must be all hex chars, even length, and at least 8 chars if ($hex.Length -lt 8 -or $hex.Length % 2 -ne 0 -or $hex -notmatch '^[0-9A-Fa-f]+$') { return $null } # Pure numeric strings could be valid decimal serials — skip those if ($hex -notmatch '[A-Fa-f]') { return $null } try { $bytes = New-Object byte[] ($hex.Length / 2) for ($i = 0; $i -lt $hex.Length; $i += 2) { $bytes[$i / 2] = [Convert]::ToByte($hex.Substring($i, 2), 16) } # Try direct decode $directAscii = -join ($bytes | ForEach-Object { if ($_ -ge 0x20 -and $_ -le 0x7E) { [char]$_ } else { '.' } }) $directPrintable = ($directAscii.ToCharArray() | Where-Object { $_ -ne '.' }).Count # Try byte-swapped decode (NVMe convention: each 2-byte word has bytes swapped) $swapped = New-Object byte[] $bytes.Length for ($i = 0; $i -lt $bytes.Length - 1; $i += 2) { $swapped[$i] = $bytes[$i + 1]; $swapped[$i + 1] = $bytes[$i] } if ($bytes.Length % 2 -eq 1) { $swapped[$bytes.Length - 1] = $bytes[$bytes.Length - 1] } $swapAscii = -join ($swapped | ForEach-Object { if ($_ -ge 0x20 -and $_ -le 0x7E) { [char]$_ } else { '.' } }) $swapPrintable = ($swapAscii.ToCharArray() | Where-Object { $_ -ne '.' }).Count $totalChars = $bytes.Length # Pick the better decoding; require at least 75% printable ASCII $bestAscii = if ($directPrintable -ge $swapPrintable) { $directAscii } else { $swapAscii } $bestCount = [math]::Max($directPrintable, $swapPrintable) if ($bestCount -ge [math]::Ceiling($totalChars * 0.75)) { $result = ($bestAscii -replace '[\.\x00]+$', '').Trim() if ($result.Length -ge 4 -and $result -notmatch '\.\.') { return $result } } } catch { } return $null } function Get-MotherboardSlotInfo { param( [int]$RamModulesUsed = 0, [int]$NvmeDrivesUsed = 0, [int]$SataDrivesUsed = 0 ) $info = @{ RAMSlotsTotal = 0 RAMSlotsUsed = $RamModulesUsed PCIeSlotsTotal = 0 M2SlotsTotal = 0 M2SlotsUsed = $NvmeDrivesUsed SATAPortsEstimated = 0 SATADrivesUsed = $SataDrivesUsed Notes = @() } try { $memArray = Get-CimInstance Win32_PhysicalMemoryArray -ErrorAction SilentlyContinue | Select-Object -First 1 if ($memArray -and $memArray.MemoryDevices) { $info.RAMSlotsTotal = [int]$memArray.MemoryDevices } } catch { } if (-not $info.RAMSlotsTotal -or $info.RAMSlotsTotal -le 0) { $info.RAMSlotsTotal = [math]::Max($info.RAMSlotsUsed, 2) $info.Notes += "RAM slot count is estimated (Win32_PhysicalMemoryArray not available)." } $systemSlots = @() try { $systemSlots = @(Get-CimInstance Win32_SystemSlot -ErrorAction SilentlyContinue) } catch { $systemSlots = @() } if ($systemSlots.Count -gt 0) { $pci = @($systemSlots | Where-Object { ($_.SlotDesignation -match '(?i)PCI') -or ($_.Description -match '(?i)PCI') }) $info.PCIeSlotsTotal = $pci.Count $m2 = @($systemSlots | Where-Object { ($_.SlotDesignation -match '(?i)M\.?2|NGFF') -or ($_.Description -match '(?i)M\.?2|NGFF') }) $info.M2SlotsTotal = $m2.Count } if (-not $info.M2SlotsTotal -or $info.M2SlotsTotal -le 0) { if ($info.M2SlotsUsed -gt 0) { $info.M2SlotsTotal = $info.M2SlotsUsed $info.Notes += "M.2/NVMe slot count is estimated from detected NVMe drives." } else { $info.M2SlotsTotal = 0 $info.Notes += "M.2/NVMe slot count could not be detected reliably on Windows." } } try { $ideControllers = @(Get-CimInstance Win32_IDEController -ErrorAction SilentlyContinue) if ($ideControllers.Count -gt 0) { $info.SATAPortsEstimated = [int]($ideControllers.Count * 2) $info.Notes += "SATA ports are estimated from IDE/AHCI controllers (rough heuristic)." } } catch { } return $info } #endregion #region Main Diagnostic Tests # Ensure WMI is available before any CIM/WMI queries (best-effort) $testData.WmiService = Ensure-WmiServiceRunning if ($testData.WmiService -and $testData.WmiService.ElevationRequested) { Write-Host "" Write-Host $testData.WmiService.Message -ForegroundColor Yellow Write-Host "" exit } Show-Progress -Status "System Information..." -Step 1 $computerInfo = Get-ComputerInfo $os = Get-CimInstance Win32_OperatingSystem $cpu = Get-CimInstance Win32_Processor $bios = Get-CimInstance Win32_BIOS # Get Motherboard info $motherboard = $null try { $motherboard = Get-CimInstance Win32_BaseBoard } catch { $motherboard = $null } # Get Serial Number (BIOS/System) $serialNumber = "Not Available" try { $serialNumber = Get-SanitizedSerial $bios.SerialNumber } catch { $serialNumber = "Not Available" } # Get Motherboard Serial Number $motherboardSerial = "Not Available" try { if ($motherboard) { $motherboardSerial = Get-SanitizedSerial $motherboard.SerialNumber } } catch { $motherboardSerial = "Not Available" } # Get CPU Processor ID $cpuProcessorId = "Not Available" try { $cpuProcessorId = Get-SanitizedSerial $cpu.ProcessorId } catch { $cpuProcessorId = "Not Available" } # Get Windows Activation Status $activationStatus = "Unknown" $licenseType = "Unknown" try { $license = Get-CimInstance -ClassName SoftwareLicensingProduct -ErrorAction SilentlyContinue | Where-Object { $_.PartialProductKey -and $_.Name -like "*Windows*" } | Select-Object -First 1 if ($license) { $activationStatus = switch ($license.LicenseStatus) { 0 { "Unlicensed" } 1 { "Activated" } 2 { "Out of Grace" } 3 { "Out of Tolerance" } 4 { "Non-Genuine" } 5 { "Notification" } 6 { "Extended Grace" } default { "Unknown" } } # Determine license type from Description if ($license.Description -match "RETAIL") { $licenseType = "Retail" } elseif ($license.Description -match "OEM") { $licenseType = "OEM" } elseif ($license.Description -match "VOLUME|MAK|KMS") { $licenseType = "Volume" } elseif ($license.Description -match "TIMEBASED") { $licenseType = "Subscription" } else { $licenseType = "Unknown" } } } catch { $activationStatus = "Unknown" $licenseType = "Unknown" } $testData.System = @{ Name = $computerInfo.CsName Manufacturer = $computerInfo.CsManufacturer Model = $computerInfo.CsModel OS = "$($os.Caption)" Version = $os.Version BIOSVersion = $bios.SMBIOSBIOSVersion SerialNumber = $serialNumber ActivationStatus = $activationStatus LicenseType = $licenseType MotherboardSerial = $motherboardSerial MotherboardManufacturer = if ($motherboard) { $motherboard.Manufacturer } else { "N/A" } MotherboardProduct = if ($motherboard) { $motherboard.Product } else { "N/A" } } Show-Progress -Status "CPU Performance & Benchmark..." $loads = @() for ($i = 0; $i -lt 5; $i++) { $loads += (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue Start-Sleep -Milliseconds 500 } $avgLoad = [math]::Round(($loads | Measure-Object -Average).Average, 1) $cpuBenchmark = Test-CPUBenchmark $testData.CPU = @{ Name = $cpu.Name Cores = $cpu.NumberOfCores Threads = $cpu.NumberOfLogicalProcessors Speed = $cpu.MaxClockSpeed CurrentLoad = $avgLoad L2Cache = if ($cpu.L2CacheSize) { "$([math]::Round($cpu.L2CacheSize / 1024, 1)) MB" } else { "N/A" } L3Cache = if ($cpu.L3CacheSize) { "$([math]::Round($cpu.L3CacheSize / 1024, 1)) MB" } else { "N/A" } Virtualization = if ($cpu.VirtualizationFirmwareEnabled) { "Enabled" } else { "Disabled" } Sockets = if ($cpu.SocketDesignation) { $cpu.SocketDesignation } else { "1" } BenchmarkOps = $cpuBenchmark.OpsPerSecond BenchmarkScore = $cpuBenchmark.Score ProcessorId = $cpuProcessorId } $cpuTier = Get-HardwareTier -ComponentType "CPU" -ComponentName $cpu.Name -Database $hardwareDB $testData.CPU.Tier = $cpuTier.Tier $testData.CPU.TierScore = $cpuTier.Score $testData.CPU.Category = $cpuTier.Category $testData.CPU.EstimatedAge = (Get-Date).Year - $cpuTier.EstimatedYear $cpuScore = if ($avgLoad -lt 70) { 7 } else { 4 } $globalScore += $cpuScore $globalScoreMax += 7 Show-Progress -Status "Memory (RAM)..." $ramModules = Get-CimInstance Win32_PhysicalMemory $totalRAM = [math]::Round(($ramModules | Measure-Object -Property Capacity -Sum).Sum / 1GB, 2) $ramSpeed = if ($ramModules[0].Speed) { $ramModules[0].Speed } else { "Unknown" } $ramType = "Unknown" if ($ramModules[0].SMBIOSMemoryType) { $ramType = switch ($ramModules[0].SMBIOSMemoryType) { 20 { "DDR" }; 21 { "DDR2" }; 24 { "DDR3" } 26 { "DDR4" }; 34 { "DDR5" } default { "Unknown" } } } # Fallback: MemoryType property (Win10 20H1+) if ($ramType -eq "Unknown") { try { $mt = $ramModules[0].MemoryType if ($mt) { $ramType = switch ([int]$mt) { 20 { "DDR" }; 21 { "DDR2" }; 22 { "DDR2 FB-DIMM" } 24 { "DDR3" }; 26 { "DDR4" }; 34 { "DDR5" }; 35 { "DDR5" } default { "Unknown" } } } } catch { } } # Fallback: speed-based heuristic if ($ramType -eq "Unknown" -and $ramSpeed -and $ramSpeed -ne "Unknown") { $spdVal = 0; try { $spdVal = [int]($ramSpeed -replace '[^0-9]', '') } catch { $spdVal = 0 } if ($spdVal -ge 4800) { $ramType = "DDR5" } elseif ($spdVal -ge 2133) { $ramType = "DDR4" } elseif ($spdVal -ge 800) { $ramType = "DDR3" } elseif ($spdVal -ge 400) { $ramType = "DDR2" } } # Build RAM module details with serial numbers $ramModuleDetails = @() foreach ($mod in $ramModules) { $modCapacity = if ($mod.Capacity) { "$([math]::Round($mod.Capacity / 1GB, 1)) GB" } else { "Unknown" } $modManufacturer = if ($mod.Manufacturer -and $mod.Manufacturer.Trim() -ne "") { $mod.Manufacturer.Trim() } else { "Unknown" } $modPartNumber = if ($mod.PartNumber -and $mod.PartNumber.Trim() -ne "") { $mod.PartNumber.Trim() } else { "N/A" } $modSlot = if ($mod.DeviceLocator) { $mod.DeviceLocator } else { "Slot" } $modSerial = Get-SanitizedSerial $mod.SerialNumber $ramModuleDetails += @{ Slot = $modSlot Capacity = $modCapacity Manufacturer = $modManufacturer PartNumber = $modPartNumber SerialNumber = $modSerial Speed = if ($mod.Speed) { "$($mod.Speed) MHz" } else { "Unknown" } } } $testData.Memory = @{ Total = "$totalRAM GB" Modules = $ramModules.Count Speed = "$ramSpeed MHz" Type = $ramType ModuleDetails = $ramModuleDetails } $ramString = "$([math]::Round($totalRAM, 0))GB $ramType" # Try benchmark-backed exact RAM match using module part numbers / descriptions, then fall back to capacity-based tiers. $ramTier = $null $ramCandidates = @() try { foreach ($m in $ramModules) { $pn = $null; $man = $null; $desc = $null try { $pn = ("" + $m.PartNumber).Trim() } catch { $pn = $null } try { $man = ("" + $m.Manufacturer).Trim() } catch { $man = $null } try { $desc = ("" + $m.Description).Trim() } catch { $desc = $null } if ($man -and $pn) { $ramCandidates += ("$man $pn") } if ($pn) { $ramCandidates += $pn } if ($desc) { $ramCandidates += $desc } } } catch { } $ramCandidates = @($ramCandidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique) foreach ($cand in $ramCandidates) { $t = Get-HardwareTier -ComponentType "RAM" -ComponentName $cand -Database $hardwareDB if ($t -and ($t.Sources -or $t.Benchmarks -or $t.Specs) -and $t.Tier -ne "Unknown") { if (-not $ramTier -or ([int]$t.Score -gt [int]$ramTier.Score)) { $ramTier = $t } } } if (-not $ramTier -or $ramTier.Tier -eq "Unknown") { $ramTier = Get-HardwareTier -ComponentType "RAM" -ComponentName @{ DDRType = $ramType; SpeedMHz = $ramSpeed } -Database $hardwareDB } if (-not $ramTier -or $ramTier.Tier -eq "Unknown") { $ramTier = Get-HardwareTier -ComponentType "RAM" -ComponentName $ramString -Database $hardwareDB } # Hardcoded heuristic fallback when all DB lookups fail if (-not $ramTier -or $ramTier.Tier -eq "Unknown") { $heurTier = "Budget"; $heurScore = 30; $heurCat = "RAM" $spdVal = 0; try { $spdVal = [int]($ramSpeed -replace '[^0-9]', '') } catch { $spdVal = 0 } $capGB = [math]::Round($totalRAM, 0) if ($ramType -eq "DDR5") { if ($capGB -ge 32) { $heurTier = "Flagship"; $heurScore = 95; $heurCat = "Workstation/High-End" } elseif ($capGB -ge 16) { $heurTier = "High Performance"; $heurScore = 85; $heurCat = "Gaming/Content Creation" } elseif ($capGB -ge 8) { $heurTier = "Mainstream"; $heurScore = 70; $heurCat = "Standard DDR5" } else { $heurTier = "Entry Level"; $heurScore = 55; $heurCat = "Low-Cap DDR5" } } elseif ($ramType -eq "DDR4") { if ($capGB -ge 32) { $heurTier = "High Performance"; $heurScore = 80; $heurCat = "Content Creation" } elseif ($capGB -ge 16) { $heurTier = "Mainstream"; $heurScore = 70; $heurCat = "Standard 2025" } elseif ($capGB -ge 8) { $heurTier = "Entry Level"; $heurScore = 50; $heurCat = "Minimum Recommended" } else { $heurTier = "Budget"; $heurScore = 30; $heurCat = "Insufficient" } } elseif ($ramType -eq "DDR3") { if ($capGB -ge 16) { $heurTier = "Entry Level"; $heurScore = 45; $heurCat = "Legacy Adequate" } elseif ($capGB -ge 8) { $heurTier = "Entry Level"; $heurScore = 40; $heurCat = "Below Standard" } else { $heurTier = "Budget"; $heurScore = 25; $heurCat = "Very Limited" } } else { if ($capGB -ge 16) { $heurTier = "Entry Level"; $heurScore = 50; $heurCat = "RAM" } elseif ($capGB -ge 8) { $heurTier = "Entry Level"; $heurScore = 45; $heurCat = "RAM" } else { $heurTier = "Budget"; $heurScore = 30; $heurCat = "RAM" } } # Bonus for high speeds if ($spdVal -ge 6000 -and $heurScore -lt 95) { $heurScore = [math]::Min($heurScore + 10, 95) } elseif ($spdVal -ge 4800 -and $heurScore -lt 90) { $heurScore = [math]::Min($heurScore + 5, 90) } elseif ($spdVal -ge 3600 -and $heurScore -lt 85) { $heurScore = [math]::Min($heurScore + 3, 85) } $ramTier = @{ Tier = $heurTier; Score = $heurScore; Category = $heurCat; EstimatedYear = 2020 } } $testData.Memory.Tier = $ramTier.Tier $testData.Memory.TierScore = $ramTier.Score $testData.Memory.Category = $ramTier.Category try { if ($ramTier.Benchmarks) { $testData.Memory.Benchmarks = $ramTier.Benchmarks } } catch { } try { if ($ramTier.Sources) { $testData.Memory.Sources = $ramTier.Sources } } catch { } try { if ($ramTier.Specs) { $testData.Memory.Specs = $ramTier.Specs } } catch { } $ramScore = if ($totalRAM -ge 8) { 7 } elseif ($totalRAM -ge 4) { 4 } else { 2 } $globalScore += $ramScore $globalScoreMax += 7 Show-Progress -Status "Storage Performance..." # ============================================================ # ENHANCED STORAGE DETECTION - Detects ALL physical disks # ============================================================ $allDisks = @() # Method 1: Get-PhysicalDisk (best for NVMe and modern drives) $physicalDisks = Get-PhysicalDisk -ErrorAction SilentlyContinue foreach ($disk in $physicalDisks) { # Determine media type more accurately $mediaType = "Unknown" $busType = $disk.BusType # NVMe is always an SSD; ensure we don't lose this by trusting only MediaType. if ($busType -eq "NVMe") { $mediaType = "NVMe SSD" } # Check MediaType property first if ($mediaType -eq "Unknown" -and ($disk.MediaType -eq "SSD" -or $disk.MediaType -eq "Solid State Drive")) { if ($busType -eq "SATA" -or $busType -eq "SAS") { $mediaType = "SATA SSD" } else { $mediaType = "SSD" } } elseif ($disk.MediaType -eq "HDD" -or $disk.MediaType -eq "Hard Disk Drive") { $mediaType = "HDD" } elseif ($disk.MediaType -eq "Unspecified" -or $disk.MediaType -eq "Unknown") { # Use BusType to determine - NVMe is always SSD if ($busType -eq "NVMe") { $mediaType = "NVMe SSD" } elseif ($busType -eq "SATA" -or $busType -eq "SAS") { # Check spindle speed or model name for SATA drives if ($disk.SpindleSpeed -eq 0 -or $disk.FriendlyName -match "SSD|Solid State|NVMe|M\.2") { $mediaType = "SATA SSD" } else { $mediaType = "HDD" } } elseif ($busType -eq "USB") { $mediaType = "USB Drive" } else { # Default based on model name patterns if ($disk.FriendlyName -match "SSD|Solid State|NVMe|M\.2|Samsung 9[678]0|WD Black SN|Crucial|Kingston") { $mediaType = "SSD" } else { $mediaType = "HDD" } } } $allDisks += @{ Model = $disk.FriendlyName Size = [math]::Round($disk.Size / 1GB, 2) SizeFormatted = "$([math]::Round($disk.Size / 1GB, 2)) GB" MediaType = $mediaType BusType = $busType Health = $disk.HealthStatus DiskNumber = $null DeviceId = $disk.DeviceId SerialNumber = $disk.SerialNumber IsBootDrive = $false } } # Method 1b: Win32_DiskDrive cross-check (helps when NVMe BusType is hidden behind RAID/storage drivers) try { $diskDrives = @(Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction SilentlyContinue) if ($diskDrives -and $diskDrives.Count -gt 0 -and $allDisks -and $allDisks.Count -gt 0) { $normSerial2 = { param($s) if ($null -eq $s) { return "" } return (($s -as [string]) -replace '[^A-Za-z0-9]', '').ToUpperInvariant() } foreach ($d in $allDisks) { try { if (-not $d) { continue } if ([string]$d.MediaType -match '(?i)NVMe') { continue } $match = $null $dSer = &$normSerial2 $d.SerialNumber if ($dSer) { $match = $diskDrives | Where-Object { (&$normSerial2 $_.SerialNumber) -eq $dSer } | Select-Object -First 1 } if (-not $match -and $d.Model) { $match = $diskDrives | Where-Object { $_.Model -and $_.Model -eq $d.Model } | Select-Object -First 1 } if ($match) { $isNvme = $false try { if ($match.PNPDeviceID -match '(?i)NVME') { $isNvme = $true } elseif ($match.Model -match '(?i)NVMe') { $isNvme = $true } } catch { } if ($isNvme) { $d.MediaType = 'NVMe SSD' $d.BusType = 'NVMe' } } } catch { } } } } catch { } # Method 1c: Cross-reference Get-PhysicalDisk rows to Get-Disk (to get reliable DiskNumber) try { $osDisks = @(Get-Disk -ErrorAction SilentlyContinue) if ($osDisks -and $osDisks.Count -gt 0 -and $allDisks -and $allDisks.Count -gt 0) { $normSerial3 = { param($s) if ($null -eq $s) { return "" } return (($s -as [string]) -replace '[^A-Za-z0-9]', '').ToUpperInvariant() } foreach ($d in $allDisks) { try { if (-not $d) { continue } if ($d.ContainsKey('DiskNumber') -and $null -ne $d.DiskNumber) { continue } $matchDisk = $null $dSer = &$normSerial3 $d.SerialNumber if ($dSer) { $matchDisk = $osDisks | Where-Object { $_.SerialNumber -and (&$normSerial3 $_.SerialNumber) -eq $dSer } | Select-Object -First 1 } if (-not $matchDisk -and $d.Model) { $matchDisk = $osDisks | Where-Object { $_.FriendlyName -and $_.FriendlyName -eq $d.Model } | Select-Object -First 1 } if ($matchDisk) { $d.DiskNumber = [int]$matchDisk.Number } } catch { } } } } catch { } # Method 2: Cross-reference with Get-Disk to find boot drive try { # Reset first to guarantee ONLY one [BOOT] label. foreach ($d in $allDisks) { $d.IsBootDrive = $false } $windowsDrive = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).SystemDrive $bootPartition = Get-Partition -DriveLetter $windowsDrive.TrimEnd(':') -ErrorAction SilentlyContinue if ($bootPartition) { $bootDiskNumber = $bootPartition.DiskNumber $bootDisk = $null try { $bootDisk = Get-Disk -Number $bootDiskNumber -ErrorAction SilentlyContinue } catch { } # Prefer exact disk-number match (most reliable) $bootMatch = $allDisks | Where-Object { $_.DiskNumber -ne $null -and $_.DiskNumber -eq $bootDiskNumber } | Select-Object -First 1 # Fallback: unique serial match if (-not $bootMatch -and $bootDisk -and $bootDisk.SerialNumber) { $normSerial = { param($s) if ($null -eq $s) { return "" } return (($s -as [string]) -replace '[^A-Za-z0-9]', '').ToUpperInvariant() } $bootSerialNorm = (&$normSerial $bootDisk.SerialNumber) $serialMatches = @($allDisks | Where-Object { $_.SerialNumber -and (&$normSerial $_.SerialNumber) -eq $bootSerialNorm }) if ($serialMatches.Count -eq 1) { $bootMatch = $serialMatches[0] } } # Last resort: only accept a UNIQUE model match if (-not $bootMatch -and $bootDisk -and $bootDisk.FriendlyName) { $modelMatches = @($allDisks | Where-Object { $_.Model -and ($_.Model -eq $bootDisk.FriendlyName) }) if ($modelMatches.Count -eq 1) { $bootMatch = $modelMatches[0] } } if ($bootMatch) { $bootMatch.IsBootDrive = $true } } } catch {} # Sort: Boot drive first, then by size (largest first) $allDisks = $allDisks | Sort-Object -Property @{Expression={$_.IsBootDrive}; Descending=$true}, @{Expression={$_.Size}; Descending=$true} # Select primary disk (prefer boot disk; if unknown, prefer fastest type) $primaryDisk = $allDisks | Where-Object { $_.IsBootDrive } | Select-Object -First 1 if (-not $primaryDisk) { $primaryDisk = $allDisks | Where-Object { $_.MediaType -match "(?i)NVMe" } | Sort-Object -Property Size -Descending | Select-Object -First 1 } if (-not $primaryDisk) { $primaryDisk = $allDisks | Where-Object { $_.MediaType -match "(?i)SSD" } | Sort-Object -Property Size -Descending | Select-Object -First 1 } if (-not $primaryDisk) { $primaryDisk = $allDisks | Select-Object -First 1 } # Calculate total storage $totalStorageGB = ($allDisks | Measure-Object -Property Size -Sum).Sum $ssdCount = ($allDisks | Where-Object { $_.MediaType -match "SSD|NVMe" }).Count $hddCount = ($allDisks | Where-Object { $_.MediaType -eq "HDD" }).Count $nvmeCount = ($allDisks | Where-Object { $_.MediaType -match "NVMe" -or $_.BusType -eq "NVMe" }).Count # Motherboard expansion/slot summary (best-effort / estimated) $sataDrivesUsed = ($allDisks | Where-Object { $_.BusType -match "SATA|SAS" -or $_.MediaType -match "(?i)HDD|SATA SSD" }).Count $testData.MotherboardSlots = Get-MotherboardSlotInfo -RamModulesUsed $ramModules.Count -NvmeDrivesUsed $nvmeCount -SataDrivesUsed $sataDrivesUsed # Run IO test on the SystemDrive (more stable than $env:TEMP which can be redirected) $systemDrive = $null try { $systemDrive = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).SystemDrive } catch { $systemDrive = $env:SystemDrive } if (-not $systemDrive) { $systemDrive = "C:" } $testDir = Join-Path ($systemDrive + "\") "Users\Public\LaptopDiagnosticTemp" try { if (-not (Test-Path $testDir)) { New-Item -ItemType Directory -Path $testDir -Force | Out-Null } } catch { $testDir = $env:TEMP } $testFile = Join-Path $testDir "speed_test.tmp" # Larger test sizes reduce caching effects (still kept reasonable for a quick diagnostic) $testSize = 32MB if ($primaryDisk.MediaType -match "(?i)NVMe") { $testSize = 64MB } elseif ($primaryDisk.MediaType -match "(?i)SSD") { $testSize = 48MB } # Initialize storage data with ALL disks $testData.Storage = @{ Model = $primaryDisk.Model Size = $primaryDisk.SizeFormatted # Preserve detailed media type so NVMe vs SATA SSD is not lost (affects tier scoring and UI) MediaType = $primaryDisk.MediaType DetailedType = $primaryDisk.MediaType BusType = $primaryDisk.BusType Health = $primaryDisk.Health WriteSpeed = "N/A" ReadSpeed = "N/A" IsBootDrive = $primaryDisk.IsBootDrive SerialNumber = Get-SanitizedSerial $primaryDisk.SerialNumber AllDisks = $allDisks TotalDisks = $allDisks.Count TotalStorageGB = [math]::Round($totalStorageGB, 0) SSDCount = $ssdCount HDDCount = $hddCount NVMeCount = $nvmeCount } try { $randomData = New-Object byte[] $testSize (New-Object Random).NextBytes($randomData) $writeTimer = [System.Diagnostics.Stopwatch]::StartNew() [System.IO.File]::WriteAllBytes($testFile, $randomData) $writeTimer.Stop() $writeSpeed = [math]::Round(($testSize / 1MB) / ($writeTimer.ElapsedMilliseconds / 1000), 2) $testData.Storage.WriteSpeed = "$writeSpeed MB/s" $readTimer = [System.Diagnostics.Stopwatch]::StartNew() $null = [System.IO.File]::ReadAllBytes($testFile) $readTimer.Stop() $readSpeed = [math]::Round(($testSize / 1MB) / ($readTimer.ElapsedMilliseconds / 1000), 2) $testData.Storage.ReadSpeed = "$readSpeed MB/s" Remove-Item $testFile -Force -ErrorAction SilentlyContinue $avgSpeed = [math]::Round(($writeSpeed + $readSpeed) / 2, 0) $testData.Storage.AvgSpeed = $avgSpeed } catch { $testData.Storage.WriteSpeed = "Test Failed" $testData.Storage.ReadSpeed = "Test Failed" # Conservative defaults when the quick IO test fails. # NVMe should not tier like SATA SSD/HDD just because the test failed. if ($testData.Storage.MediaType -match "(?i)NVMe") { $avgSpeed = 2500 } elseif ($testData.Storage.MediaType -match "(?i)SSD") { $avgSpeed = 550 } else { $avgSpeed = 120 } $testData.Storage.AvgSpeed = $avgSpeed } $storageString = "$([math]::Round($primaryDisk.Size, 0))|$($testData.Storage.MediaType)|$avgSpeed" $storageTierInput = @{ Model = $primaryDisk.Model; LegacyString = $storageString } $storageTier = Get-HardwareTier -ComponentType "Storage" -ComponentName $storageTierInput -Database $hardwareDB # Hardcoded heuristic fallback when all DB lookups fail if (-not $storageTier -or $storageTier.Tier -eq "Unknown") { $sType = $testData.Storage.MediaType $sSpd = 0; try { $sSpd = [int]$avgSpeed } catch { $sSpd = 0 } if ($sType -match "(?i)NVMe") { if ($sSpd -ge 5000) { $storageTier = @{ Tier = "Flagship"; Score = 95; Category = "Professional Grade NVMe" } } elseif ($sSpd -ge 3000) { $storageTier = @{ Tier = "High Performance"; Score = 85; Category = "High-Speed NVMe" } } elseif ($sSpd -ge 1500) { $storageTier = @{ Tier = "Mainstream"; Score = 75; Category = "Fast NVMe" } } else { $storageTier = @{ Tier = "Mid-Range"; Score = 65; Category = "NVMe SSD" } } } elseif ($sType -match "(?i)SSD") { if ($sSpd -ge 500) { $storageTier = @{ Tier = "Mid-Range"; Score = 60; Category = "Standard SSD" } } elseif ($sSpd -ge 200) { $storageTier = @{ Tier = "Entry Level"; Score = 50; Category = "Slow SSD" } } else { $storageTier = @{ Tier = "Entry Level"; Score = 45; Category = "SATA SSD" } } } else { if ($sSpd -ge 100) { $storageTier = @{ Tier = "Budget"; Score = 30; Category = "Mechanical Drive" } } else { $storageTier = @{ Tier = "Legacy"; Score = 20; Category = "Very Slow" } } } } $testData.Storage.Tier = $storageTier.Tier $testData.Storage.TierScore = $storageTier.Score $testData.Storage.Category = $storageTier.Category try { if ($storageTier.Benchmarks) { $testData.Storage.Benchmarks = $storageTier.Benchmarks } } catch { } try { if ($storageTier.Sources) { $testData.Storage.Sources = $storageTier.Sources } } catch { } try { if ($storageTier.Specs) { $testData.Storage.Specs = $storageTier.Specs } } catch { } # Score based on best storage (NVMe > SSD > HDD) $storageScore = if ($nvmeCount -gt 0) { 10 } elseif ($ssdCount -gt 0) { 8 } else { 4 } $globalScore += $storageScore $globalScoreMax += 10 Show-Progress -Status "Battery Health..." $batteryData = Get-BatteryInfo $testData.Battery = $batteryData $batteryScore = 0 $batteryStatus = "No Battery" if ($batteryData.Detected) { if ($batteryData.Health -ge 85) { $batteryScore = 8; $batteryStatus = "Excellent" } elseif ($batteryData.Health -ge 70) { $batteryScore = 6; $batteryStatus = "Good" } elseif ($batteryData.Health -ge 50) { $batteryScore = 4; $batteryStatus = "Fair" } else { $batteryScore = 2; $batteryStatus = "Poor" } $globalScore += $batteryScore $globalScoreMax += 8 } else { # Desktop / no battery: do not award or deduct points. $batteryScore = 0 $batteryStatus = "No Battery" } # Store for reporting/debugging $testData.Battery.Score = $batteryScore $testData.Battery.ScoreMax = if ($batteryData.Detected) { 8 } else { 0 } Show-Progress -Status "AC Adapter..." $testData.Charger = Get-ChargerInfo Show-Progress -Status "Network & WiFi..." $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } $testData.Network = @{ Adapters = $adapters | ForEach-Object { @{ Name = $_.Name Description = $_.InterfaceDescription Speed = "$($_.LinkSpeed)" MAC = $_.MacAddress } } WiFi = @{ Detected = $false Name = "Not Found" Speed = "N/A" Status = "N/A" SignalStrength = "N/A" } } # FIXED WiFi detection - more comprehensive $allAdapters = Get-NetAdapter $wifiAdapter = $allAdapters | Where-Object { $_.InterfaceDescription -notmatch "Ethernet|Gigabit|10/100|LAN|RJ45|GbE|Virtual|Bluetooth" -and ($_.InterfaceDescription -match "Wi-?Fi|Wireless|802\.11|WLAN|Centrino|Advanced-N|Dual.?Band|Killer|Qualcomm|Atheros|Realtek.*RT|Intel.*Wireless|Broadcom.*BCM43" -or $_.Name -match "^Wi-?Fi$|^Wireless$|^WLAN$") } | Select-Object -First 1 if ($wifiAdapter) { $testData.Network.WiFi.Detected = $true $testData.Network.WiFi.Name = $wifiAdapter.InterfaceDescription $testData.Network.WiFi.Speed = if ($wifiAdapter.LinkSpeed) { $wifiAdapter.LinkSpeed } else { "N/A" } $testData.Network.WiFi.Status = $wifiAdapter.Status try { $wifiInfo = netsh wlan show interfaces 2>$null if ($wifiInfo -match "Signal\s+:\s+(\d+)%") { $testData.Network.WiFi.SignalStrength = "$($matches[1])%" } if ($wifiInfo -match "Receive rate.*?:\s+([\d.]+)") { $testData.Network.WiFi.ReceiveRate = "$($matches[1]) Mbps" } if ($wifiInfo -match "Transmit rate.*?:\s+([\d.]+)") { $testData.Network.WiFi.TransmitRate = "$($matches[1]) Mbps" } } catch {} } $globalScore += 5 $globalScoreMax += 5 $wifiMax = if ($wifiAdapter) { 4 } else { 0 } $globalScore += if ($testData.Network.WiFi.Detected) { 4 } else { 0 } $globalScoreMax += $wifiMax # Bluetooth try { $bluetooth = Get-PnpDevice -Class Bluetooth -Status OK -ErrorAction SilentlyContinue $testData.Network.Bluetooth = @{ Detected = if ($bluetooth) { $true } else { $false } Devices = if ($bluetooth) { $bluetooth.Count } else { 0 } } } catch { $testData.Network.Bluetooth = @{ Detected = $false; Devices = 0 } } Show-Progress -Status "Audio Devices..." $audioDevices = Get-CimInstance Win32_SoundDevice $testData.Audio = @{ Devices = $audioDevices | ForEach-Object { @{ Name = $_.Name Status = $_.Status } } } $globalScore += 5 $globalScoreMax += 5 Show-Progress -Status "Input Devices..." # Filter out virtual/system devices to count only physical keyboards and mice $allKeyboards = Get-PnpDevice -Class Keyboard -Status OK -ErrorAction SilentlyContinue $allMice = Get-PnpDevice -Class Mouse -Status OK -ErrorAction SilentlyContinue # Patterns to exclude virtual/system devices $virtualPatterns = @( "HID Keyboard Device", "HID-compliant", "PS/2", "Virtual", "Root", "Composite", "System", "Remote Desktop", "Terminal Server", "Microsoft Hardware", "Standard PS/2", "PC/AT Enhanced" ) # Count only physical keyboards (USB keyboards typically have manufacturer names) $physicalKeyboards = @($allKeyboards | Where-Object { $name = $_.FriendlyName $isVirtual = $false foreach ($pattern in $virtualPatterns) { if ($name -match $pattern) { $isVirtual = $true; break } } -not $isVirtual }) # Count only physical mice (USB mice typically have manufacturer names) $physicalMice = @($allMice | Where-Object { $name = $_.FriendlyName $isVirtual = $false foreach ($pattern in $virtualPatterns) { if ($name -match $pattern) { $isVirtual = $true; break } } -not $isVirtual }) # If no physical devices found but virtual ones exist, count at least 1 as system input $keyboardCount = if ($physicalKeyboards.Count -gt 0) { $physicalKeyboards.Count } elseif ($allKeyboards.Count -gt 0) { 1 } else { 0 } $mouseCount = if ($physicalMice.Count -gt 0) { $physicalMice.Count } elseif ($allMice.Count -gt 0) { 1 } else { 0 } $testData.InputDevices = @{ Keyboards = $keyboardCount PointingDevices = $mouseCount } $globalScore += 4 $globalScoreMax += 4 Show-Progress -Status "Temperature & Cooling..." try { $testData.Temperature = Get-TemperatureInfo } catch { # Never allow temperature detection to break the report; treat as sensor-not-found. $testData.Temperature = @{ CPUTemp = "N/A" Status = "Sensor Not Found" Warning = $false CelsiusValue = 0 Source = "Get-TemperatureInfo failed" Notes = @("$($_.Exception.Message)") StressTest = @{ Before = 0; After = 0; Delta = 0; Tested = $false } } } $tempScore = 0 $tempMax = 0 if ($testData.Temperature -and -not [string]::IsNullOrWhiteSpace([string]$testData.Temperature.CPUTemp) -and $testData.Temperature.CPUTemp -ne "N/A") { $tempMax = 7 if ($testData.Temperature.CelsiusValue -lt 65) { $tempScore = 7 } elseif ($testData.Temperature.CelsiusValue -lt 75) { $tempScore = 5 } elseif ($testData.Temperature.CelsiusValue -lt 85) { $tempScore = 3 } else { $tempScore = 1 } } $globalScore += $tempScore $globalScoreMax += $tempMax Show-Progress -Status "Display..." $video = Get-CimInstance Win32_VideoController | Select-Object -First 1 $monitors = Get-CimInstance WmiMonitorID -Namespace root\wmi -ErrorAction SilentlyContinue # Calculate screen size in inches $screenSizeInches = "N/A" try { $monitorSize = Get-CimInstance WmiMonitorBasicDisplayParams -Namespace root\wmi -ErrorAction SilentlyContinue if ($monitorSize) { $widthCm = $monitorSize.MaxHorizontalImageSize $heightCm = $monitorSize.MaxVerticalImageSize if ($widthCm -gt 0 -and $heightCm -gt 0) { $diagonalCm = [math]::Sqrt([math]::Pow($widthCm, 2) + [math]::Pow($heightCm, 2)) $diagonalInches = [math]::Round($diagonalCm / 2.54, 1) $screenSizeInches = "$diagonalInches inches" } } } catch {} # Check for touchscreen $touchscreen = $false try { $touch = Get-PnpDevice | Where-Object { $_.Class -eq "HIDClass" -and ($_.FriendlyName -match "Touch|HID-compliant touch" -or $_.HardwareID -match "VID_&.*&MI_01") } if ($touch) { $touchscreen = $true } } catch {} $testData.Display = @{ GPU = $video.Name Resolution = "$($video.CurrentHorizontalResolution)x$($video.CurrentVerticalResolution)" RefreshRate = $video.CurrentRefreshRate VRAM = if ($video.AdapterRAM) { "$([math]::Round($video.AdapterRAM / 1GB, 2)) GB" } else { "N/A" } ScreenSize = $screenSizeInches Touchscreen = $touchscreen } $displayString = "$($video.CurrentHorizontalResolution)x$($video.CurrentVerticalResolution)@$($video.CurrentRefreshRate)" $displayTier = Get-HardwareTier -ComponentType "Display" -ComponentName $displayString -Database $hardwareDB $testData.Display.Tier = $displayTier.Tier $testData.Display.TierScore = $displayTier.Score $testData.Display.Category = $displayTier.Category $globalScore += 5 $globalScoreMax += 5 Show-Progress -Status "Graphics Card..." $gpuType = if ($video.Name -match "NVIDIA|AMD|Radeon|GeForce|RTX|GTX|Arc") { "Dedicated" } else { "Integrated" } $gpuDeviceId = "Not Available" try { $rawPnp = $null # Try PNPDeviceID first (most reliable hardware identifier for GPUs) if ($video.PNPDeviceID) { $rawPnp = $video.PNPDeviceID.Trim() } # Fallback: DeviceID property if (-not $rawPnp -and $video.DeviceID) { $rawPnp = $video.DeviceID.Trim() } if ($rawPnp) { # Extract clean VEN/DEV/SUBSYS/REV identifiers $venId = ""; $devId = ""; $subSys = ""; $revId = "" if ($rawPnp -match "VEN_([0-9A-Fa-f]+)") { $venId = $matches[1].ToUpper() } if ($rawPnp -match "DEV_([0-9A-Fa-f]+)") { $devId = $matches[1].ToUpper() } if ($rawPnp -match "SUBSYS_([0-9A-Fa-f]+)") { $subSys = $matches[1].ToUpper() } if ($rawPnp -match "REV_([0-9A-Fa-f]+)") { $revId = $matches[1].ToUpper() } if ($venId -and $devId) { $gpuDeviceId = "VEN:$venId DEV:$devId" if ($subSys) { $gpuDeviceId += " SUBSYS:$subSys" } if ($revId) { $gpuDeviceId += " REV:$revId" } } else { $gpuDeviceId = $rawPnp } } } catch { $gpuDeviceId = "Not Available" } $testData.GPU = @{ Name = $video.Name Type = $gpuType VRAM = $testData.Display.VRAM Driver = $video.DriverVersion Status = $video.Status DeviceID = $gpuDeviceId } $gpuTier = Get-HardwareTier -ComponentType "GPU" -ComponentName $video.Name -Database $hardwareDB $testData.GPU.Tier = $gpuTier.Tier $testData.GPU.TierScore = $gpuTier.Score $testData.GPU.Category = $gpuTier.Category try { if ($gpuTier.Specs) { $testData.GPU.Specs = $gpuTier.Specs } } catch { } try { if ($gpuTier.Benchmarks) { $testData.GPU.Benchmarks = $gpuTier.Benchmarks } } catch { } try { if ($gpuTier.Sources) { $testData.GPU.Sources = $gpuTier.Sources } } catch { } # Parse GPU display connector counts from dbgpu specs when available try { $connStr = $null if ($testData.GPU.Specs) { try { $connStr = ("" + $testData.GPU.Specs.displayConnectors).Trim() } catch { $connStr = $null } } if ($connStr) { $testData.GPU.DisplayConnectorCounts = Get-DisplayConnectorCounts -ConnectorString $connStr } } catch { } $gpuScore = if ($gpuType -eq "Dedicated") { 7 } else { 4 } $globalScore += $gpuScore $globalScoreMax += $gpuScore Show-Progress -Status "Webcam Detection..." # Detect webcam even if disabled $cameras = Get-PnpDevice -FriendlyName *camera*,*webcam*,*imaging* | Where-Object { $_.Class -match "Camera|Image" } $testData.Webcam = @{ Detected = if ($cameras.Count -gt 0) { $true } else { $false } Count = $cameras.Count Devices = $cameras | ForEach-Object { @{ Name = $_.FriendlyName Status = $_.Status Enabled = ($_.Status -eq "OK") } } } # Only score webcam presence on portable systems (avoid penalizing desktops) $webcamMax = if ($batteryData.Detected) { 3 } else { 0 } $globalScore += if ($cameras.Count -gt 0) { $webcamMax } else { 0 } $globalScoreMax += $webcamMax Show-Progress -Status "Ports & Connectors..." # ============================================================ # ENHANCED USB PORT DETECTION - More accurate counting # ============================================================ $usbControllers = Get-CimInstance Win32_USBController -ErrorAction SilentlyContinue $allUSBDevices = Get-PnpDevice -Class USB -ErrorAction SilentlyContinue # Initialize USB port counts $usbACount = 0 $usbCCount = 0 $thunderboltCount = 0 # Method 1: Detect USB-C and Thunderbolt ports specifically # Only count actual USB-C controller/port devices, NOT USB 3.x devices $usbCDevices = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { # Strict matching for USB-C specific devices only ($_.FriendlyName -match "USB.*Type.?C.*Connector|USB-C.*Port|UCSI|USB Connector Manager" -or $_.FriendlyName -match "Thunderbolt.*Controller|Thunderbolt.*Port") -and $_.Status -eq "OK" } # Count Thunderbolt controllers (each typically represents 1-2 ports) $thunderboltControllers = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match "Thunderbolt.*Controller|Thunderbolt.*Host" -and $_.Status -eq "OK" } $thunderboltCount = $thunderboltControllers.Count # Count USB-C connectors (UCSI = USB Type-C Connector System Interface) $ucsiDevices = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match "UCSI|USB Connector Manager|USB Type-C" -and $_.Status -eq "OK" } # USB-C count: UCSI devices indicate actual USB-C ports # Don't count Thunderbolt separately if UCSI already includes them if ($ucsiDevices.Count -gt 0) { $usbCCount = $ucsiDevices.Count } elseif ($thunderboltCount -gt 0) { # Thunderbolt ports are also USB-C $usbCCount = $thunderboltCount } else { # No USB-C detected $usbCCount = 0 } # Method 2: Count USB-A ports by analyzing USB host controllers # USB-A ports are typically on xHCI, EHCI, or UHCI controllers $usbHostControllers = Get-CimInstance Win32_USBController -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "xHCI|EHCI|UHCI|USB.*Host Controller|USB.*Controller" } # Get root hubs to estimate USB-A ports $usbHubs = Get-CimInstance Win32_USBHub -ErrorAction SilentlyContinue $rootHubs = $usbHubs | Where-Object { $_.Description -match "Root Hub" } # For workstations/desktops, use a more generous estimate # Check if this is a workstation (multiple controllers, Xeon CPU, etc.) $isWorkstation = $false try { $cpuInfo = Get-CimInstance Win32_Processor | Select-Object -First 1 if ($cpuInfo.Name -match "Xeon|Threadripper|EPYC" -or $rootHubs.Count -gt 4) { $isWorkstation = $true } } catch {} if ($isWorkstation) { # Workstations typically have many USB-A ports # Each xHCI controller can support 4-8 ports, EHCI 2-4 ports $xhciCount = ($usbHostControllers | Where-Object { $_.Name -match "xHCI|USB 3" }).Count $ehciCount = ($usbHostControllers | Where-Object { $_.Name -match "EHCI|USB 2" }).Count # Conservative estimate: xHCI = 4 ports each, EHCI = 2 ports each $usbACount = ($xhciCount * 4) + ($ehciCount * 2) # Cap at reasonable maximum for workstations $usbACount = [math]::Min($usbACount, 12) $usbACount = [math]::Max($usbACount, 6) # Workstations have at least 6 USB-A } else { # Laptops: more conservative estimate if ($rootHubs.Count -gt 0) { if ($rootHubs.Count -eq 1) { $usbACount = 2 } elseif ($rootHubs.Count -eq 2) { $usbACount = 3 } elseif ($rootHubs.Count -eq 3) { $usbACount = 4 } else { $usbACount = [math]::Min($rootHubs.Count + 2, 6) } } else { $usbACount = 3 # Default for laptops } } $usbPortCount = $usbACount + $usbCCount # Check for USB-C Power Delivery $usbCPD = $false try { $pdDevices = Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match "Power Delivery|USB-C.*PD|Thunderbolt.*Charging|UCSI.*PD" } if ($pdDevices.Count -gt 0) { $usbCPD = $true } } catch {} # Detect HDMI - Enhanced detection with multiple fallbacks $hasHDMI = $false try { # Method 1: Check video outputs $videoOutputs = Get-CimInstance Win32_VideoController foreach ($video in $videoOutputs) { if ($video.VideoProcessor -match "HDMI" -or $video.Name -match "HDMI" -or $video.Description -match "HDMI") { $hasHDMI = $true break } } # Method 2: Check PnP devices for HDMI if (-not $hasHDMI) { $hdmiDevices = Get-PnpDevice | Where-Object { $_.FriendlyName -match "HDMI" -or $_.HardwareID -match "HDMI" } if ($hdmiDevices.Count -gt 0) { $hasHDMI = $true } } # Method 3: Check monitor connections if (-not $hasHDMI) { $monitors = Get-CimInstance WmiMonitorConnectionParams -Namespace root\wmi -ErrorAction SilentlyContinue if ($monitors) { foreach ($mon in $monitors) { if ($mon.VideoOutputTechnology -eq 5) { # 5 = HDMI $hasHDMI = $true break } } } } # Method 4: Heuristic - Most laptops from 2010+ have HDMI # Check CPU generation to estimate laptop age if (-not $hasHDMI -and $cpu.Name) { # Intel i3/i5/i7 4th gen (4xxx) and newer typically have HDMI # AMD Ryzen and newer typically have HDMI if ($cpu.Name -match "i[3579]-[4-9]\d{3}|i[3579]-1[0-4]\d{3}|Ryzen") { $hasHDMI = $true } } # Method 5: If laptop is not ultra-thin and has reasonable specs, likely has HDMI if (-not $hasHDMI) { # Check if laptop has dedicated GPU or decent integrated graphics $hasDecentGPU = $false foreach ($video in $videoOutputs) { if ($video.Name -match "NVIDIA|AMD|Radeon|HD Graphics [4-9]|Iris|UHD") { $hasDecentGPU = $true break } } if ($hasDecentGPU) { $hasHDMI = $true # Laptops with decent GPUs almost always have HDMI } } } catch { # Conservative fallback: assume HDMI exists on laptops (it's very common) $hasHDMI = $true } # Detect VGA port - Enhanced detection $hasVGA = $false try { # Method 1: Check PnP devices for VGA $vgaDevices = Get-PnpDevice | Where-Object { $_.FriendlyName -match "VGA|Video Graphics|D-Sub" -or $_.HardwareID -match "VGA" } if ($vgaDevices -and $vgaDevices.Count -gt 0) { $hasVGA = $true } # Method 2: Check video controller for VGA support if (-not $hasVGA) { $videoControllers = Get-CimInstance Win32_VideoController foreach ($video in $videoControllers) { if ($video.VideoProcessor -match "VGA" -or $video.AdapterCompatibility -match "VGA" -or $video.Name -match "VGA" -or $video.Description -match "VGA") { $hasVGA = $true break } } } # Method 3: Check monitor connections for VGA (analog) if (-not $hasVGA) { $monitors = Get-CimInstance WmiMonitorConnectionParams -Namespace root\wmi -ErrorAction SilentlyContinue if ($monitors) { foreach ($mon in $monitors) { # VideoOutputTechnology: 0 = HD15 (VGA), 1 = S-video, etc. if ($mon.VideoOutputTechnology -eq 0) { $hasVGA = $true break } } } } # Method 4: Heuristic based on laptop age # Laptops from 2008-2017 commonly had VGA if (-not $hasVGA -and $cpu.Name) { # Intel 2nd gen (2xxx) through 7th gen (7xxx) often had VGA if ($cpu.Name -match "i[3579]-[2-7]\d{3}[A-Z]*") { $hasVGA = $true } } # Method 5: If has HDMI and is older generation (pre-2018), likely has VGA too # Most business laptops from 2010-2017 had both HDMI and VGA if (-not $hasVGA -and $hasHDMI) { # Check if it's an older laptop (not 8th gen or newer) if ($cpu.Name -match "i[3579]-[2-7]\d{3}|AMD.*A[6-9]|AMD.*E[12]|Pentium|Celeron") { $hasVGA = $true # Older laptops with HDMI typically also have VGA } } # Method 6: Check system model for business laptops (they often have VGA) if (-not $hasVGA) { $model = $computerInfo.CsModel if ($model -match "ThinkPad|Latitude|EliteBook|ProBook|Precision|Vostro") { # Business laptops often retained VGA longer $hasVGA = $true } } } catch { $hasVGA = $false } # Detect Ethernet port - Enhanced detection $hasEthernet = $false try { # Method 1: Check all network adapters $allAdapters = Get-NetAdapter -ErrorAction SilentlyContinue $ethAdapter = $allAdapters | Where-Object { $_.InterfaceDescription -match "Ethernet|Gigabit|10/100|LAN|RJ45|Realtek.*PCIe GBE|Intel.*Ethernet" -and $_.InterfaceDescription -notmatch "Virtual|Wireless|WiFi|Bluetooth|Miniport" } if ($ethAdapter.Count -gt 0) { $hasEthernet = $true } # Method 2: Check PnP devices for Ethernet controller if (-not $hasEthernet) { $ethDevices = Get-PnpDevice -Class Net | Where-Object { $_.FriendlyName -match "Ethernet|Gigabit|10/100|LAN|Network.*Adapter" -and $_.FriendlyName -notmatch "Wireless|WiFi|Virtual|Bluetooth|Miniport" } if ($ethDevices.Count -gt 0) { $hasEthernet = $true } } # Method 3: Check CIM for network adapter if (-not $hasEthernet) { $netAdapters = Get-CimInstance Win32_NetworkAdapter | Where-Object { $_.Name -match "Ethernet|Gigabit|10/100|LAN|Realtek" -and $_.Name -notmatch "Wireless|WiFi|Virtual|Bluetooth|Miniport|802\.11" } if ($netAdapters.Count -gt 0) { $hasEthernet = $true } } } catch { $hasEthernet = $false } # Detect DisplayPort (best-effort) $hasDisplayPort = $false try { $monitors = Get-CimInstance WmiMonitorConnectionParams -Namespace root\wmi -ErrorAction SilentlyContinue if ($monitors) { foreach ($mon in $monitors) { # VideoOutputTechnology: 10 = DisplayPort (external), 11 = DisplayPort (embedded) if ($mon.VideoOutputTechnology -eq 10 -or $mon.VideoOutputTechnology -eq 11) { $hasDisplayPort = $true break } } } if (-not $hasDisplayPort) { $dpDevices = @(Get-PnpDevice -ErrorAction SilentlyContinue | Where-Object { $_.FriendlyName -match 'DisplayPort' }) if ($dpDevices.Count -gt 0) { $hasDisplayPort = $true } } } catch { $hasDisplayPort = $false } $gpuDisplay = @{ HDMI = 0; DisplayPort = 0; DVI = 0; VGA = 0; USBC = 0; Total = 0; Source = "None" } try { if ($testData.GPU -and $testData.GPU.DisplayConnectorCounts) { $gpuDisplay = $testData.GPU.DisplayConnectorCounts } } catch { } $testData.Ports = @{ USB = @{ Total = $usbPortCount USBA = $usbACount USBC = $usbCCount Thunderbolt = $thunderboltCount Controllers = $usbControllers.Count } USBC_PD = $usbCPD HDMI = $hasHDMI VGA = $hasVGA Ethernet = $hasEthernet DisplayPort = $hasDisplayPort DisplayOutputs = @{ HDMI = [int]$gpuDisplay.HDMI DisplayPort = [int]$gpuDisplay.DisplayPort DVI = [int]$gpuDisplay.DVI VGA = [int]$gpuDisplay.VGA USBC_DPAlt = [int]$gpuDisplay.USBC Total = [int]$gpuDisplay.Total Source = ("" + $gpuDisplay.Source) } } $globalScore += 4 $globalScoreMax += 4 Show-Progress -Status "Keyboard Features..." # Keyboard backlight detection - Fixed $keyboardBacklight = $false try { # Only check for actual keyboard backlight devices, NOT monitor brightness $backlightDevice = Get-PnpDevice | Where-Object { $_.FriendlyName -match "Keyboard.*Backlight|Keyboard.*Illumination|Keyboard.*Light" -and $_.FriendlyName -notmatch "Monitor|Display|Screen" } if ($backlightDevice -and $backlightDevice.Count -gt 0) { $keyboardBacklight = $true } # Check for keyboard backlight in WMI (not monitor brightness!) if (-not $keyboardBacklight) { $kbBacklight = Get-CimInstance -Namespace root\wmi -ClassName MSI_Keyboard_Backlight -ErrorAction SilentlyContinue if ($kbBacklight) { $keyboardBacklight = $true } } # Check for gaming keyboard RGB control if (-not $keyboardBacklight) { $rgbDevice = Get-PnpDevice | Where-Object { $_.FriendlyName -match "RGB.*Keyboard|Gaming.*Keyboard.*Light|Illuminated Keyboard" } if ($rgbDevice -and $rgbDevice.Count -gt 0) { $keyboardBacklight = $true } } } catch {} # Keyboard layout check (Ctrl position) $keyboardLayout = "Standard" try { $kbDevices = Get-PnpDevice -Class Keyboard -Status OK -ErrorAction SilentlyContinue foreach ($kb in $kbDevices) { if ($kb.FriendlyName -match "ThinkPad|Dell|HP|Lenovo") { $keyboardLayout = "Standard (Ctrl bottom-left)" break } } } catch {} $testData.Keyboard = @{ Backlight = $keyboardBacklight Layout = $keyboardLayout Count = $testData.InputDevices.Keyboards } Show-Progress -Status "Durability & Certifications..." # Check for MIL-STD certification $milStdCertified = $false $certifications = @() try { # Check BIOS/System info for certifications $systemInfo = Get-CimInstance Win32_ComputerSystem $biosInfo = Get-CimInstance Win32_BIOS $combinedInfo = "$($systemInfo.Model) $($systemInfo.Manufacturer) $($biosInfo.Version)" if ($combinedInfo -match "MIL-?STD|810G|810H|Military|Rugged") { $milStdCertified = $true $certifications += "MIL-STD-810G/H" } # Common rugged laptop brands if ($systemInfo.Model -match "ThinkPad.*T\d|ThinkPad.*X\d|ThinkPad.*P\d|Latitude.*Rugged|EliteBook|ProBook|ToughBook") { $certifications += "Business/Rugged Grade" } } catch {} $testData.Durability = @{ MIL_STD_Certified = $milStdCertified Certifications = $certifications EstimatedLifespan = if ($milStdCertified) { "4-6 years" } else { "3-4 years" } } Show-Progress -Status "USB Power Delivery..." # Check if USB ports can charge when laptop is off $usbPowerWhenOff = $false try { # This is usually a BIOS feature, hard to detect from Windows # Check for "Always On USB" or "PowerShare" features $usbFeatures = Get-PnpDevice | Where-Object { $_.FriendlyName -match "Always On|PowerShare|Charging Port" } if ($usbFeatures.Count -gt 0) { $usbPowerWhenOff = $true } } catch {} $testData.Features = @{ USBPowerWhenOff = $usbPowerWhenOff USBC_PDCharging = $usbCPD } Show-Progress -Status "Calculating Performance Score..." $weights = @{ CPU = 0.30 RAM = 0.20 Storage = 0.15 GPU = 0.20 Display = 0.10 Other = 0.05 } $performanceScore = [math]::Round(( ($testData.CPU.TierScore * $weights.CPU) + ($testData.Memory.TierScore * $weights.RAM) + ($testData.Storage.TierScore * $weights.Storage) + ($testData.GPU.TierScore * $weights.GPU) + ($testData.Display.TierScore * $weights.Display) + (50 * $weights.Other) ), 0) $testData.PerformanceScore = $performanceScore $testData.PerformanceTier = if ($performanceScore -ge 85) { "Flagship" } elseif ($performanceScore -ge 70) { "High Performance" } elseif ($performanceScore -ge 55) { "Mainstream" } elseif ($performanceScore -ge 40) { "Mid-Range" } elseif ($performanceScore -ge 25) { "Entry Level" } else { "Legacy" } Show-Progress -Status "Generating Recommendations..." $useCases = Get-UseCaseRecommendations -PerformanceScore $performanceScore -Database $upgradeDB $testData.UseCases = $useCases $currentSpecs = @{ RAM = [math]::Round($totalRAM, 0) StorageType = $testData.Storage.MediaType HasSSD = ($testData.Storage.SSDCount -gt 0) HasNVMe = ($testData.Storage.NVMeCount -gt 0) StorageSpeed = $avgSpeed BatteryHealth = $batteryData.Health CPUAge = $testData.CPU.EstimatedAge PerformanceScore = $performanceScore } $upgrades = Get-UpgradeRecommendations -CurrentSpecs $currentSpecs -Database $upgradeDB $testData.UpgradeRecommendations = $upgrades Show-Progress -Status $(if ($batteryData.Detected) { "AC Adapter Health Test..." } else { "Power / PSU Info..." }) if ($batteryData.Detected) { $testData.AdapterTest = Test-AdapterHealth } else { $testData.AdapterTest = Get-DesktopPowerInfo } Show-Progress -Status "Finalizing Report..." #endregion #region Calculate Final Scores # Normalize the additive score into a real /100 so modern/new machines are not under-scored. $testData.HealthScoreRaw = $globalScore $testData.HealthScoreMax = $globalScoreMax if (-not $globalScoreMax -or $globalScoreMax -le 0) { $globalScore = 0 } else { $globalScore = [math]::Round(($globalScore / $globalScoreMax) * 100, 0) $globalScore = [math]::Max(0, [math]::Min(100, $globalScore)) } $testData.HealthScore = $globalScore $overallHealthRating = if ($globalScore -ge 85) { "Excellent" } elseif ($globalScore -ge 70) { "Good" } elseif ($globalScore -ge 50) { "Fair" } else { "Needs Attention" } #endregion $reportPath = Join-Path $outputDir "Diagnostic_Report_$timestamp.html" # Generate HTML report # Prepare HTML variables $cpuScoreVal = $cpuScore $cpuStatus = if ($avgLoad -lt 70) { "Good" } else { "Fair" } $ramScoreVal = $ramScore $ramStatus = if ($totalRAM -ge 8) { "Excellent" } elseif ($totalRAM -ge 4) { "Good" } else { "Low" } $storageScoreVal = $storageScore $storageStatus = if ($testData.Storage.MediaType -match "(?i)SSD|NVMe") { "Excellent" } else { "Standard" } $batteryScoreVal = if ($batteryData.Detected) { $batteryScore } else { 0 } $batteryDetails = if ($batteryData.Detected) { "$($batteryData.Health)% Health" } else { "Not applicable" } $cpuScoreMax = 7 $ramScoreMax = 7 $storageScoreMax = 10 $batteryScoreMax = if ($batteryData.Detected) { 8 } else { 0 } $cpuScorePct = if ($cpuScoreMax -gt 0) { [math]::Round(($cpuScoreVal / $cpuScoreMax) * 100, 0) } else { 0 } $ramScorePct = if ($ramScoreMax -gt 0) { [math]::Round(($ramScoreVal / $ramScoreMax) * 100, 0) } else { 0 } $storageScorePct = if ($storageScoreMax -gt 0) { [math]::Round(($storageScoreVal / $storageScoreMax) * 100, 0) } else { 0 } $batteryScorePct = if ($batteryScoreMax -gt 0) { [math]::Round(($batteryScoreVal / $batteryScoreMax) * 100, 0) } else { 0 } $batteryBreakdownHtml = "" if ($batteryScoreMax -gt 0) { $batteryBreakdownHtml = @" Battery
$batteryScoreVal/$batteryScoreMax "@ } $healthScoreNoteHtml = "
Points Earned: $($testData.HealthScoreRaw)/$($testData.HealthScoreMax). Overall Health Score = (Points Earned / Points Possible) × 100. Battery is only scored when present.
" $tempScoreVal = $tempScore $tempStatus = $testData.Temperature.Status $gpuScoreVal = $gpuScore $gpuStatus = if ($gpuType -eq "Dedicated") { "Dedicated" } else { "Integrated" } $webcamScoreVal = 0 $webcamStatus = "Optional" $webcamDetails = "Not scored on desktops" if ($webcamMax -gt 0) { $webcamScoreVal = if ($testData.Webcam.Detected) { $webcamMax } else { 0 } $webcamStatus = if ($testData.Webcam.Detected) { "Detected" } else { "Not Found" } $webcamDetails = "$($testData.Webcam.Count) camera(s)" } $wifiScoreVal = 0 $wifiStatus = "Optional" $wifiDetails = "N/A" if ($wifiMax -gt 0) { $wifiScoreVal = if ($testData.Network.WiFi.Detected) { $wifiMax } else { 0 } $wifiStatus = if ($testData.Network.WiFi.Detected) { "Detected" } else { "Not Found" } $wifiDetails = if ($testData.Network.WiFi.Detected) { $testData.Network.WiFi.Name } else { "N/A" } } # Generate use cases HTML $useCasesHTML = "" if ($testData.UseCases.Suitable -and $testData.UseCases.Suitable.Count -gt 0) { $useCasesHTML += "

+ Well Suited For:

    " foreach ($useCase in $testData.UseCases.Suitable) { $useCasesHTML += "
  • $useCase
  • " } $useCasesHTML += "
" } if ($testData.UseCases.Limited -and $testData.UseCases.Limited.Count -gt 0) { $useCasesHTML += "

! Limited Performance For:

    " foreach ($useCase in $testData.UseCases.Limited) { $useCasesHTML += "
  • $useCase
  • " } $useCasesHTML += "
" } if ($testData.UseCases.NotRecommended -and $testData.UseCases.NotRecommended.Count -gt 0) { $useCasesHTML += "

- Not Recommended For:

" } # Generate upgrades HTML $upgradesHTML = "" if ($testData.UpgradeRecommendations -and $testData.UpgradeRecommendations.Count -gt 0) { foreach ($upgrade in $testData.UpgradeRecommendations) { $priority = if ($upgrade.Priority -eq 1) { "HIGH" } else { "MEDIUM" } $priorityColor = if ($upgrade.Priority -eq 1) { "#e74c3c" } else { "#f39c12" } $upgradesHTML += @"

$($upgrade.Component)

Priority: $priority
Current: $($upgrade.Current)
Recommended: $($upgrade.Recommended)
Cost: $($upgrade.Cost)
Performance Gain: +$($upgrade.PerformanceGain)%

$($upgrade.Description)

"@ } } else { $upgradesHTML = "

No critical upgrades needed at this time.

" } # Generate HTML report $htmlContent = @" Laptop Diagnostic Report - v15.0

Hardware Diagnostic Report

Complete System Analysis - Generated: $timestampDisplay

System: $($testData.System.Manufacturer) $($testData.System.Model)

Serial Number: $($testData.System.SerialNumber)

Health Score

Current condition and functionality

$globalScore/100
$overallHealthRating
CPU
$cpuScoreVal/$cpuScoreMax RAM
$ramScoreVal/$ramScoreMax Storage
$storageScoreVal/$storageScoreMax $batteryBreakdownHtml
$healthScoreNoteHtml

Performance Score

Compared to 2025 standards

$performanceScore/100
$($testData.PerformanceTier)
CPU ($($testData.CPU.Tier))
$($testData.CPU.TierScore)/100 RAM ($($testData.Memory.Tier))
$($testData.Memory.TierScore)/100 Storage ($($testData.Storage.Tier))
$($testData.Storage.TierScore)/100 GPU ($($testData.GPU.Tier))
$($testData.GPU.TierScore)/100

System Overview

System Information

Manufacturer $($testData.System.Manufacturer)
Model $($testData.System.Model)
Operating System $($testData.System.OS)
BIOS Version $($testData.System.BIOSVersion)
Activation Status $($testData.System.ActivationStatus)
License Type $($testData.System.LicenseType)
Motherboard $($testData.System.MotherboardManufacturer) $($testData.System.MotherboardProduct)
Motherboard S/N $($testData.System.MotherboardSerial)

Processor

Model $($testData.CPU.Name)
Cores / Threads $($testData.CPU.Cores) / $($testData.CPU.Threads)
Speed $($testData.CPU.Speed) MHz
Current Load $($testData.CPU.CurrentLoad)%
Benchmark $($testData.CPU.BenchmarkOps) ops/sec
Tier $($testData.CPU.Tier) ($($testData.CPU.TierScore)/100)
Age ~$($testData.CPU.EstimatedAge) years old
Processor ID $($testData.CPU.ProcessorId)

Memory (RAM)

Total RAM $($testData.Memory.Total)
Type $($testData.Memory.Type)
Speed $($testData.Memory.Speed)
Modules $($testData.Memory.Modules)
Tier $($testData.Memory.Tier) ($($testData.Memory.TierScore)/100)
$( $memExtra = "" try { if ($testData.Memory.Category) { $memExtra += "
Category$($testData.Memory.Category)
" } } catch { } try { if ($testData.Memory.Sources) { $src = @($testData.Memory.Sources) -join ', ' if (-not [string]::IsNullOrWhiteSpace($src)) { $memExtra += "
Sources$src
" } } } catch { } try { if ($testData.Memory.Specs -and $testData.Memory.Specs.DDRType) { $memExtra += "
PassMark DDR Type$($testData.Memory.Specs.DDRType)
" } } catch { } try { if ($testData.Memory.Benchmarks) { $lat = $null; $rd = $null; $wr = $null try { $lat = $testData.Memory.Benchmarks.latencyNs } catch { $lat = $null } try { $rd = $testData.Memory.Benchmarks.readGBs } catch { $rd = $null } try { $wr = $testData.Memory.Benchmarks.writeGBs } catch { $wr = $null } if ($lat -or $rd -or $wr) { $memExtra += "
PassMark (Derived)Read $rd GB/s · Write $wr GB/s · Latency $lat ns
" } } } catch { } $memExtra ) $( $ramModuleHtml = "" if ($testData.Memory.ModuleDetails -and $testData.Memory.ModuleDetails.Count -gt 0) { $ramModuleHtml += "
Module Details
" foreach ($mod in $testData.Memory.ModuleDetails) { $ramModuleHtml += "
$($mod.Slot)
$($mod.Manufacturer) · $($mod.Capacity) · $($mod.Speed)
Part Number $($mod.PartNumber)   Serial $($mod.SerialNumber)
" } } $ramModuleHtml )

Storage ($($testData.Storage.TotalDisks) Drive$(if($testData.Storage.TotalDisks -gt 1){'s'}))

Total Capacity $($testData.Storage.TotalStorageGB) GB
NVMe SSDs $($testData.Storage.NVMeCount)
SATA SSDs $($testData.Storage.SSDCount - $testData.Storage.NVMeCount)
HDDs $($testData.Storage.HDDCount)
Boot Drive $($testData.Storage.Model) ($($testData.Storage.DetailedType))
Boot Drive Size $($testData.Storage.Size)
Boot Drive S/N $($testData.Storage.SerialNumber)
Health $($testData.Storage.Health)
Read Speed $($testData.Storage.ReadSpeed)
Write Speed $($testData.Storage.WriteSpeed)
Tier $($testData.Storage.Tier) ($($testData.Storage.TierScore)/100)
$( $stExtra = "" try { if ($testData.Storage.Category) { $stExtra += "
Category$($testData.Storage.Category)
" } } catch { } try { if ($testData.Storage.Sources) { $src = @($testData.Storage.Sources) -join ', ' if (-not [string]::IsNullOrWhiteSpace($src)) { $stExtra += "
Sources$src
" } } } catch { } try { if ($testData.Storage.Specs -and $testData.Storage.Specs.Type) { $stExtra += "
PassMark Type$($testData.Storage.Specs.Type)
" } if ($testData.Storage.Specs -and $testData.Storage.Specs.Size) { $stExtra += "
PassMark Size$($testData.Storage.Specs.Size)
" } } catch { } try { if ($testData.Storage.Benchmarks) { $dm = $null try { $dm = $testData.Storage.Benchmarks.passMarkDiskMark } catch { $dm = $null } if ($dm) { $stExtra += "
PassMark Disk Mark$dm
" } } } catch { } $stExtra ) $( $allDisksHtml = "" foreach ($disk in $testData.Storage.AllDisks) { $bootLabel = if ($disk.IsBootDrive) { " [BOOT]" } else { "" } $diskSn = Get-SanitizedSerial $disk.SerialNumber $allDisksHtml += "
$($disk.MediaType) · $($disk.Model)$bootLabel
$($disk.SizeFormatted)
Serial $diskSn
" } $allDisksHtml )

Graphics

GPU $($testData.GPU.Name)
Type $($testData.GPU.Type)
VRAM $($testData.GPU.VRAM)
$( try { $s = $testData.GPU.Specs $b = $testData.GPU.Benchmarks $rows = "" if ($s) { try { if ($s.manufacturer) { $rows += "
Manufacturer$($s.manufacturer)
" } } catch { } try { if ($s.architecture) { $rows += "
Architecture$($s.architecture)
" } } catch { } try { if ($s.processNodeNm) { $rows += "
Process Node$($s.processNodeNm) nm
" } } catch { } try { if ($s.shaderUnits) { $rows += "
Shader Units$($s.shaderUnits)
" } } catch { } try { if ($s.tensorCores) { $rows += "
Tensor Cores$($s.tensorCores)
" } } catch { } try { if ($s.rayTracingCores) { $rows += "
RT Cores$($s.rayTracingCores)
" } } catch { } try { if ($s.tdpW) { $rows += "
TDP$($s.tdpW) W
" } } catch { } try { if ($s.memoryType -or $s.memorySizeGB -or $s.memoryBandwidthGBs -or $s.memoryBusBits) { $memParts = @() if ($s.memorySizeGB) { $memParts += ("$($s.memorySizeGB) GB") } if ($s.memoryType) { $memParts += ("$($s.memoryType)") } if ($s.memoryBusBits) { $memParts += ("$($s.memoryBusBits)-bit") } if ($s.memoryBandwidthGBs) { $memParts += ("$([math]::Round([double]$s.memoryBandwidthGBs, 1)) GB/s") } if ($memParts.Count -gt 0) { # Avoid non-ASCII separator to prevent charset mojibake (e.g., "·") in some viewers. $rows += "
Memory$($memParts -join ' · ')
" } } } catch { } try { if ($s.displayConnectors) { $rows += "
Display Connectors$($s.displayConnectors)
" } } catch { } } if ($b) { try { if ($b.passMarkG3d) { $rows += "
PassMark G3D$($b.passMarkG3d)
" } } catch { } try { if ($b.dbgpuSingleFloatTflops) { $rows += "
Single-Precision$($b.dbgpuSingleFloatTflops) TFLOPS
" } } catch { } } $rows } catch { "" } )
Driver $($testData.GPU.Driver)
Tier $($testData.GPU.Tier) ($($testData.GPU.TierScore)/100)
Device ID $($testData.GPU.DeviceID)

Display

Resolution $($testData.Display.Resolution)
Refresh Rate $($testData.Display.RefreshRate) Hz
Screen Size $($testData.Display.ScreenSize)
Touchscreen $(if ($testData.Display.Touchscreen) { 'Yes' } else { 'No' })
Tier $($testData.Display.Tier) ($($testData.Display.TierScore)/100)

Network & Connectivity

WiFi $(if ($testData.Network.WiFi.Detected) { 'Detected' } else { 'Not Found' })
"@ if ($testData.Network.WiFi.Detected) { $htmlContent += @"
WiFi Adapter $($testData.Network.WiFi.Name)
Link Speed $($testData.Network.WiFi.Speed)
Status $($testData.Network.WiFi.Status)
"@ if ($testData.Network.WiFi.SignalStrength -ne "N/A") { $htmlContent += @"
Signal Strength $($testData.Network.WiFi.SignalStrength)
"@ } } $htmlContent += @"
Bluetooth $(if ($testData.Network.Bluetooth.Detected) { 'Detected' } else { 'Not Found' })
Ethernet $(if ($testData.Ports.Ethernet) { 'Yes' } else { 'No' })

Ports & Connectors

Total USB Ports $($testData.Ports.USB.Total) (estimated)
USB Type-A $($testData.Ports.USB.USBA) (estimated)
USB Type-C $($testData.Ports.USB.USBC)
Thunderbolt $($testData.Ports.USB.Thunderbolt)
USB-C PD $(if ($testData.Ports.USBC_PD) { 'Supported' } else { 'Not Detected' })
HDMI $(if ($testData.Ports.HDMI) { 'Yes' } else { 'Not Detected' })
VGA $(if ($testData.Ports.VGA) { 'Yes' } else { 'Not Detected' })
DisplayPort $(if ($testData.Ports.DisplayPort) { 'Yes' } else { 'Not Detected' })
Ethernet Port $(if ($testData.Ports.Ethernet) { 'Yes' } else { 'Not Detected' })
$( try { $do = $testData.Ports.DisplayOutputs if ($do -and ([int]$do.Total -gt 0)) { $h = "
GPU Display Outputs$(if ($do.Source) { $do.Source } else { 'Estimated' })
" $h += "
Total$([int]$do.Total)
" if ([int]$do.HDMI -gt 0) { $h += "
HDMI$([int]$do.HDMI)
" } if ([int]$do.DisplayPort -gt 0) { $h += "
DisplayPort$([int]$do.DisplayPort)
" } if ([int]$do.DVI -gt 0) { $h += "
DVI$([int]$do.DVI)
" } if ([int]$do.VGA -gt 0) { $h += "
VGA$([int]$do.VGA)
" } if ([int]$do.USBC_DPAlt -gt 0) { $h += "
USB-C (DP Alt)$([int]$do.USBC_DPAlt)
" } $h } } catch { "" } )

Motherboard Expansion Slots

RAM Slots $($testData.MotherboardSlots.RAMSlotsUsed) / $($testData.MotherboardSlots.RAMSlotsTotal) used
M.2 / NVMe $($testData.MotherboardSlots.M2SlotsUsed) used (slots: $($testData.MotherboardSlots.M2SlotsTotal) est.)
PCI/PCIe Slots $($testData.MotherboardSlots.PCIeSlotsTotal) detected
SATA $($testData.MotherboardSlots.SATADrivesUsed) drives (ports: $($testData.MotherboardSlots.SATAPortsEstimated) est.)

Keyboard & Input

Keyboard Backlight $(if ($testData.Keyboard.Backlight) { 'Detected' } else { 'Not Detected' })
Layout $($testData.Keyboard.Layout)
Keyboards $($testData.Keyboard.Count)

Webcam

Status $(if ($testData.Webcam.Detected) { 'Detected' } else { 'Not Found' })
Count $($testData.Webcam.Count)
"@ if ($testData.Webcam.Detected -and $testData.Webcam.Devices) { foreach ($cam in $testData.Webcam.Devices) { $htmlContent += @"
$($cam.Name) $(if ($cam.Enabled) { 'Enabled' } else { 'Disabled' })
"@ } } $htmlContent += @"

Durability & Certifications

MIL-STD Certified $(if ($testData.Durability.MIL_STD_Certified) { 'Yes' } else { 'No/Unknown' })
"@ if ($testData.Durability.Certifications -and $testData.Durability.Certifications.Count -gt 0) { foreach ($cert in $testData.Durability.Certifications) { $htmlContent += @"
Certification $cert
"@ } } $htmlContent += @"
Est. Lifespan $($testData.Durability.EstimatedLifespan)

Special Features

USB Power When Off $(if ($testData.Features.USBPowerWhenOff) { 'Supported' } else { 'Unknown' })
USB-C PD Charging $(if ($testData.Features.USBC_PDCharging) { 'Supported' } else { 'No' })

Recommended Use Cases

Based on your Performance Score of $performanceScore/100 ($($testData.PerformanceTier)), here's what your laptop can handle:

$useCasesHTML

Upgrade Recommendations

These upgrades could improve your system's performance:

$upgradesHTML

Battery Analysis

"@ if ($testData.Battery.Detected) { # Color coding: Green (>80%), Yellow (60-80%), Orange (40-60%), Red (<40%) $healthColor = if ($testData.Battery.Health -gt 80) { '#27ae60' } elseif ($testData.Battery.Health -ge 60) { '#f1c40f' } elseif ($testData.Battery.Health -ge 40) { '#e67e22' } else { '#e74c3c' } $healthStatus = if ($testData.Battery.Health -gt 80) { 'Excellent' } elseif ($testData.Battery.Health -ge 60) { 'Good' } elseif ($testData.Battery.Health -ge 40) { 'Fair' } else { 'Poor' } # Optional diagnostics (helps troubleshoot 0% health / missing capacities) $batteryDebugHtml = "" if ($testData.Battery.Debug -and $testData.Battery.Debug.Count -gt 0) { $batteryDebugLines = ($testData.Battery.Debug | ForEach-Object { "
  • $($_)
  • " }) -join "" $batteryDebugHtml = @"
    Battery Diagnostics (click to expand)
      $batteryDebugLines
    "@ } $htmlContent += @"
    $($testData.Battery.Health)%
    Battery Health
    $healthStatus
    Health = (Full Charge Capacity / Design Capacity) x 100 = ($($testData.Battery.FullChargeCapacity) / $($testData.Battery.DesignCapacity)) x 100 = $($testData.Battery.Health)%
    $batteryDebugHtml

    Capacity Information

    Design Capacity $($testData.Battery.DesignCapacity) mWh
    Full Charge Capacity $($testData.Battery.FullChargeCapacity) mWh
    Current Charge $($testData.Battery.CurrentCharge)%
    Health Percentage $($testData.Battery.Health)%

    Battery Details

    Status $($testData.Battery.Status)
    Chemistry $($testData.Battery.Chemistry)
    Voltage $($testData.Battery.Voltage)
    Current Rate $($testData.Battery.CurrentRate)
    Cycle Count $($testData.Battery.CycleCount)
    Manufacturer $($testData.Battery.Manufacturer)
    "@ } else { $htmlContent += @"

    No Battery Detected

    This system does not have a battery installed or the battery is not detected. This is normal for desktop computers.

    "@ } $htmlContent += @"
    "@ if ($batteryData.Detected) { $htmlContent += @"

    AC Adapter / Charger Health Test

    "@ if ($testData.AdapterTest.Connected) { # Determine health color $healthColor = switch ($testData.AdapterTest.Health) { "Excellent" { '#27ae60' } "Good" { '#3498db' } "Fair" { '#f39c12' } default { '#e74c3c' } } $htmlContent += @"

    Overall Adapter Health

    $($testData.AdapterTest.Health)
    10-second comprehensive test completed

    Power Delivery Analysis

    Battery Voltage

    Current $($testData.AdapterTest.Voltage.Current) V
    Average $($testData.AdapterTest.Voltage.Avg) V
    Min / Max $($testData.AdapterTest.Voltage.Min) - $($testData.AdapterTest.Voltage.Max) V
    Stability $($testData.AdapterTest.Voltage.Stability)

    Charging Power

    Current $($testData.AdapterTest.Power.Current) W
    Average $($testData.AdapterTest.Power.Avg) W
    Min / Max $($testData.AdapterTest.Power.Min) - $($testData.AdapterTest.Power.Max) W
    Estimated Rating $($testData.AdapterTest.Power.EstimatedRating)
    "@ if ($testData.AdapterTest.Current.Avg -gt 0) { $htmlContent += @"

    Current Draw

    Current $($testData.AdapterTest.Current.Current) A
    Average $($testData.AdapterTest.Current.Avg) A
    Min / Max $($testData.AdapterTest.Current.Min) - $($testData.AdapterTest.Current.Max) A
    "@ } $htmlContent += @"

    Charging Performance

    "@ if ($testData.AdapterTest.ChargingRate.IsCharging) { $htmlContent += @"

    Charging Rate

    Current Rate $($testData.AdapterTest.ChargingRate.Current) W
    Average Rate $($testData.AdapterTest.ChargingRate.Avg) W
    Min / Max $($testData.AdapterTest.ChargingRate.Min) - $($testData.AdapterTest.ChargingRate.Max) W
    Status [OK] Charging
    "@ } else { # Check if battery is at 100% - not charging is normal in this case $batteryLevel = $testData.AdapterTest.BatteryLevel.Start $isFullyCharged = $batteryLevel -ge 99 if ($isFullyCharged) { $htmlContent += @"

    Charging Status

    Status [OK] Fully Charged
    Battery Level $($batteryLevel)%

    Battery is fully charged. Not charging is normal when battery is at 100%.

    "@ } else { $htmlContent += @"

    Charging Status

    Status [X] Not Charging
    Battery Level $($batteryLevel)%

    Adapter is connected but battery is not charging at $($batteryLevel)%. This could indicate an issue with the adapter, cable, or charging port.

    "@ } } $htmlContent += @"

    Battery Level Change

    Start Level $($testData.AdapterTest.BatteryLevel.Start)%
    End Level $($testData.AdapterTest.BatteryLevel.End)%
    Delta (10s) $($testData.AdapterTest.BatteryLevel.Delta)%
    "@ if ($testData.AdapterTest.Issues.Count -gt 0) { $htmlContent += @"

    Issues Detected

    "@ foreach ($issue in $testData.AdapterTest.Issues) { $htmlContent += @"
    [!] $issue
    "@ } $htmlContent += @"
    "@ } $htmlContent += @"

    Recommendations

    "@ foreach ($recommendation in $testData.AdapterTest.Recommendations) { $htmlContent += @"
    [OK] $recommendation
    "@ } $htmlContent += @"
    "@ # Display notes if available if ($testData.AdapterTest.Notes -and $testData.AdapterTest.Notes.Count -gt 0) { $htmlContent += @"

    Important Notes

    "@ foreach ($note in $testData.AdapterTest.Notes) { $htmlContent += @"
    * $note
    "@ } $htmlContent += @"
    "@ } $htmlContent += @"

    Understanding Adapter Health:

    • Voltage Stability: Good adapters maintain stable voltage within 3-5% variance
    • Power Output: Should match or exceed the adapter's rated wattage during charging
    • Charging Rate: Healthy adapters charge at consistent rates without significant drops
    • Typical Ratings: Laptops use 45W (ultrabooks), 65W (standard), 90W+ (gaming/workstations)
    "@ } else { $htmlContent += @"

    Adapter Not Connected

    The AC adapter is not currently connected to the laptop.

    To perform the comprehensive adapter health test, please:

    1. Connect the AC adapter to your laptop
    2. Ensure the charging indicator light is on
    3. Run the diagnostic tool again

    What the Adapter Test Checks:

    • Voltage stability and fluctuations over 10 seconds
    • Power delivery capacity and consistency
    • Charging rate and efficiency
    • Battery charging behavior
    • Adapter health assessment with recommendations
    "@ } } else { $htmlContent += @"

    Power Supply (PSU) / Power Information

    Desktop Power Overview

    Windows usually cannot read internal PSU wattage/health directly. Below is the best available system power information.

    Active Power Plan

    Plan$($testData.AdapterTest.ActivePowerPlan)
    GUID$($testData.AdapterTest.ActivePowerPlanGuid)
    Source$($testData.AdapterTest.ActivePowerPlanSource)

    UPS / Battery-like Device

    Detected$([string]$testData.AdapterTest.UPSDetected)
    Name$($testData.AdapterTest.UPSName)

    PSU Wattage (Rated)

    Wattage$($testData.AdapterTest.PsuWattage)
    Source$($testData.AdapterTest.PsuWattageSource)
    If this shows N/A, Windows couldn't read the PSU's label wattage. Verify from the PSU sticker/spec sheet.

    Live Power Sensors

    Provider$($testData.AdapterTest.SensorProvider)
    Power Draw$($testData.AdapterTest.PowerDrawWatts)
    CPU Package$($testData.AdapterTest.CpuPackagePowerWatts)
    GPU$($testData.AdapterTest.GpuPowerWatts)
    Source$($testData.AdapterTest.PowerDrawSource)
    This is current power draw (if available), not the PSU's rated wattage.

    Fan Speeds

    Top Fans$(if ($testData.AdapterTest.FanReadings -and $testData.AdapterTest.FanReadings.Count -gt 0) { (@($testData.AdapterTest.FanReadings) -join '
    ') } else { 'N/A' })
    If this shows N/A, your system isn't exposing fan sensors to Windows. LibreHardwareMonitor (WMI enabled) usually fixes this.

    Notes

    "@ foreach ($note in $testData.AdapterTest.Notes) { $htmlContent += @"
    * $note
    "@ } $htmlContent += @"

    Recommendations

    "@ foreach ($rec in $testData.AdapterTest.Recommendations) { $htmlContent += @"
    [OK] $rec
    "@ } $htmlContent += @"
    "@ } $htmlContent += @"

    Detailed Hardware Information

    Temperature & Cooling

    "@ if ($testData.Temperature.CPUTemp -ne "N/A") { $tempColor = if ($testData.Temperature.CelsiusValue -lt 65) { '#27ae60' } elseif ($testData.Temperature.CelsiusValue -lt 75) { '#3498db' } elseif ($testData.Temperature.CelsiusValue -lt 85) { '#f39c12' } else { '#e74c3c' } $htmlContent += @"

    CPU Temperature

    $($testData.Temperature.CPUTemp)
    $($testData.Temperature.Status)
    "@ if ($testData.Temperature.StressTest.Tested) { $assessmentColor = switch ($testData.Temperature.StressTest.HeatingAssessment) { "Critical Issues" { '#e74c3c' } "Moderate Issues" { '#f39c12' } "Minor Issues" { '#f39c12' } "Good" { '#3498db' } "Excellent" { '#27ae60' } default { '#95a5a6' } } $htmlContent += @"

    Stress Test (15s)

    Before $($testData.Temperature.StressTest.Before)°C
    After $($testData.Temperature.StressTest.After)°C
    Delta +$($testData.Temperature.StressTest.Delta)°C
    Max Temp $($testData.Temperature.StressTest.Max)°C

    Assessment

    $($testData.Temperature.StressTest.HeatingAssessment)
    "@ } $htmlContent += @"
    "@ } else { $htmlContent += @"

    CPU Temperature

    StatusSensor Not Found
    Source$($testData.Temperature.Source)
    Why: Windows often cannot read CPU/PSU sensors on desktops without a hardware-monitor sensor provider.
    Tip: Run LibreHardwareMonitor or OpenHardwareMonitor (WMI enabled) and re-run the diagnostic.

    WMI Service

    Running$([string]$testData.WmiService.Running)
    Message$($testData.WmiService.Message)
    "@ } $htmlContent += @"

    CPU Cache

    L2 Cache $($testData.CPU.L2Cache)
    L3 Cache $($testData.CPU.L3Cache)
    Virtualization $($testData.CPU.Virtualization)
    Socket $($testData.CPU.Sockets)

    Audio Devices

    "@ foreach ($audio in $testData.Audio.Devices) { $htmlContent += @"
    $($audio.Name) $($audio.Status)
    "@ } $htmlContent += @"

    Input Devices

    Keyboards $($testData.InputDevices.Keyboards)
    Pointing Devices $($testData.InputDevices.PointingDevices)

    Network Adapters

    "@ foreach ($adapter in $testData.Network.Adapters) { $htmlContent += @"
    $($adapter.Name) $($adapter.Speed)
    "@ } $htmlContent += @"

    Keyboard Test

    Press any key to test

    Keys will turn green when pressed. Test all keys to ensure they work correctly.

    Touchpad/Mouse Test

    Draw on the canvas to test

    Click, drag, and test all mouse buttons and touchpad gestures.

    Left Click
    Not Tested
    Right Click
    Not Tested
    Movement
    Not Tested
    Drag
    Not Tested

    Display Test

    Screen Properties

    Resolution $($testData.Display.Resolution)
    Screen Size $($testData.Display.ScreenSize)
    Refresh Rate $($testData.Display.RefreshRate) Hz
    Touchscreen $(if ($testData.Display.Touchscreen) { 'Yes' } else { 'No' })

    Dead Pixel Test

    Click a color to test fullscreen. Look carefully for dead or stuck pixels. Press ESC or click to exit.

    Audio Test

    Speaker Test

    Test your laptop speakers. You should hear a 440Hz tone from the selected speaker.

    Stereo Test

    Frequency Test

    Plays a 200Hz-2000Hz sweep. You should hear the pitch increase smoothly.

    Detected Audio Devices

    "@ foreach ($audio in $testData.Audio.Devices) { $htmlContent += @"
    $($audio.Name) $($audio.Status)
    "@ } $htmlContent += @"

    Microphone Test

    Test Your Microphone

    Click "Start Test" and speak into your microphone. The level meter will show audio input.

    Microphone Selection

    Audio Level

    0%
    Not tested

    Detected Microphones

    Click "Start Test" to detect microphones...

    Webcam Test

    Test Your Webcam

    Click "Start Camera" to preview your webcam. You can switch cameras and take photos.

    Camera Selection

    Camera Info

    Status Not started
    Resolution -

    Camera Preview

    Camera preview will appear here

    Captured Photo

    Detected Cameras

    Click "Start Camera" to detect webcams...

    "@ $htmlContent | Out-File -FilePath $reportPath -Encoding UTF8 # Always save a copy to Desktop for easy access $desktopPath = [Environment]::GetFolderPath("Desktop") $desktopReportsDir = Join-Path $desktopPath "DiagnosticReports" New-Item -ItemType Directory -Path $desktopReportsDir -Force | Out-Null $reportFileName = Split-Path -Leaf $reportPath $desktopReportPath = Join-Path $desktopReportsDir $reportFileName # Copy to Desktop if not already there if ($reportPath -ne $desktopReportPath) { Copy-Item -Path $reportPath -Destination $desktopReportPath -Force } # Show completion message Show-Complete # Open the report automatically try { Start-Process $desktopReportPath } catch { # Silent fail - user can open manually } # Brief pause before exit Start-Sleep -Seconds 2