PS_HWinfo.psm1

# PS_HWinfo PowerShell Module
# Provides a hardware info dashboard similar to HWinfo64

function Show-HWTerminalDashboard {
    param([Parameter(Mandatory)][psobject]$Snapshot)
    $cpu = $Snapshot.CPU
    $mb = $Snapshot.Motherboard
    $gp = @($Snapshot.GPU)
    $mem = $Snapshot.Memory
    $monitors = @($Snapshot.Monitors)

    Write-Host ("=" * 78)
    Write-Host ("PowerShell Utility Inspired by HWinfo64 — Report: {0} " -f $Snapshot.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")) -ForegroundColor Cyan
    Write-Host "Written by: Jared Heinrichs (https://jaredheinrichs.substack.com)" -ForegroundColor DarkGray
    Write-Host ("=" * 78)

    # BIOS section first (left), Motherboard section right
    $biosLines = @()
    $biosLines += '[BIOS]'
    try {
        $biosObj = Get-WmiObject win32_bios | Select-Object -First 1
        if ($biosObj) {
            $biosLines += "SMBIOSBIOSVersion: $($biosObj.SMBIOSBIOSVersion)"
            $biosLines += "Manufacturer: $($biosObj.Manufacturer)"
            $biosLines += "Name: $($biosObj.Name)"
            $biosLines += "SerialNumber: $($biosObj.SerialNumber)"
            $biosLines += "Version: $($biosObj.Version)"
        } else {
            $biosLines += "BIOS Info: n/a"
        }
    } catch {
        $biosLines += "BIOS Info: n/a"
    }

    $mbLines = @()
    $mbLines += '[Motherboard]'
    $mbLines += "{0} {1} (S/N: {2})" -f $mb.Manufacturer, $mb.Product, ($mb.SerialNumber ?? 'n/a')
    # Chassis type mapping
    $chassisTypeMap = @{
        1 = 'Other'; 2 = 'Unknown'; 3 = 'Desktop'; 4 = 'Low Profile Desktop'; 5 = 'Pizza Box';
        6 = 'Mini Tower'; 7 = 'Tower'; 8 = 'Portable'; 9 = 'Laptop'; 10 = 'Notebook';
        11 = 'Hand Held'; 12 = 'Docking Station'; 13 = 'All in One'; 14 = 'Sub Notebook';
        15 = 'Space-saving'; 16 = 'Lunch Box'; 17 = 'Main System Chassis'; 18 = 'Expansion Chassis';
        19 = 'SubChassis'; 20 = 'Bus Expansion Chassis'; 21 = 'Peripheral Chassis'; 22 = 'Storage Chassis';
        23 = 'Rack Mount Chassis'; 24 = 'Sealed-case PC'; 25 = 'Multi-system chassis'; 26 = 'Compact PCI';
        27 = 'Advanced TCA'; 28 = 'Blade'; 29 = 'Blade Enclosure'; 30 = 'Tablet'; 31 = 'Convertible';
        32 = 'Detachable'; 33 = 'IoT Gateway'; 34 = 'Embedded PC'; 35 = 'Mini PC'; 36 = 'Stick PC'
    }
    $chassisStr = 'n/a'
    if ($mb.ChassisType) {
        $types = $mb.ChassisType -split ',' | ForEach-Object { $_.Trim() }
        $chassisStr = ($types | ForEach-Object {
            $name = $chassisTypeMap[[int]$_] ?? 'Unknown'
            "$name (Code=$_)"
        }) -join ', '
    }
    $mbLines += "Chassis: {0}" -f $chassisStr
    $mbLines += "Fans Detected: {0}" -f $mb.FansDetected

    # Print BIOS and Motherboard sections side by side
    Write-Host ""
    $maxBIOS = $biosLines.Count
    $maxMB = $mbLines.Count
    $maxLines = [Math]::Max($maxBIOS, $maxMB)
    $biosColWidth = ($biosLines | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum
    if ($biosColWidth -lt 24) { $biosColWidth = 24 }
    for ($i = 0; $i -lt $maxLines; $i++) {
        $biosCol = if ($i -lt $biosLines.Count) { $biosLines[$i] } else { '' }
        $mbCol = if ($i -lt $mbLines.Count) { $mbLines[$i] } else { '' }
        $biosColPadded = $biosCol.PadRight($biosColWidth)
        if ($i -eq 0) {
            Write-Host ("{0} {1}" -f $biosColPadded, $mbCol) -ForegroundColor Yellow
        } elseif ($i -eq 1) {
            Write-Host ("{0} {1}" -f $biosColPadded, $mbCol)
        } else {
            Write-Host ("{0} {1}" -f $biosColPadded, $mbCol)
        }
    }
    if ($mb.FanInfo) {
        $mb.FanInfo | ForEach-Object {
            $name = $_.Name
            $ds = $_.DesiredSpeed
            $vs = $_.VariableSpeed
            if (($name -and $name -ne 'Cooling Device') -or ($ds -and $ds -ne 'n/a') -or ($vs -and $vs -ne 'n/a')) {
                " - {0} DesiredSpeed: {1} Variable: {2}" -f ($name ?? 'n/a'), ($ds ?? 'n/a'), ($vs ?? 'n/a') | Write-Host
            }
        }
    }
    if ($mb.StorageTemps) {
        Write-Host "Storage Temps:" -ForegroundColor DarkYellow
        $mb.StorageTemps | ForEach-Object {
            " - {0}: {1} °C" -f $_.Drive, $_.TempC | Write-Host
        }
    }

    # CPU section
    Write-Host "" 
    Write-Host "[CPU]" -ForegroundColor Yellow
    $cpuTable = [PSCustomObject]@{
        Sockets = $cpu.Sockets
        Cores = $cpu.Cores
        Logical = $cpu.LogicalProcessors
        'Base(MHz)' = $cpu.BaseClockMHz
        'Current(MHz)' = $cpu.CurrentClockMHz
        'Load(%)' = $cpu.LoadPercent
        'L3 Cache' = $cpu.L3Cache
    }
    $cpu.Name | Write-Host
    $cpuTable | Format-Table -AutoSize

    if ($mb.FanInfo) {
        $mb.FanInfo | ForEach-Object {
            $name = $_.Name
            $ds = $_.DesiredSpeed
            $vs = $_.VariableSpeed
            if (($name -and $name -ne 'Cooling Device') -or ($ds -and $ds -ne 'n/a') -or ($vs -and $vs -ne 'n/a')) {
                " - {0} DesiredSpeed: {1} Variable: {2}" -f ($name ?? 'n/a'), ($ds ?? 'n/a'), ($vs ?? 'n/a') | Write-Host
            }
        }
    }

    if ($mb.StorageTemps) {
        Write-Host "Storage Temps:" -ForegroundColor DarkYellow
        $mb.StorageTemps | ForEach-Object {
            " - {0}: {1} °C" -f $_.Drive, $_.TempC | Write-Host
        }
    }

    # GPU(s)
    Write-Host "`n[GPU]" -ForegroundColor Yellow
    if ($gp.Count -eq 0 -or ($gp.Count -eq 1 -and -not $gp[0].Name)) {
        Write-Host "(No GPU info found)"
    } else {
        $gpuTable = foreach ($g in $gp) {
            if ($g.Name) {
                [PSCustomObject]@{
                    Name = $g.Name
                    VRAM = $g.AdapterRAM
                    Driver = $g.DriverVersion
                    AdapterCompatibility = $g.AdapterCompatibility
                }
            }
        }
        $gpuTable | Format-Table -AutoSize
    }

    # Monitors
    Write-Host "[Monitors]" -ForegroundColor Yellow
    if ($monitors.Count -eq 0 -or ($monitors.Count -eq 1 -and -not $monitors[0].DeviceName)) {
        Write-Host "(No monitor info found)"
    } else {
        $monitorTable = foreach ($m in $monitors) {
            [PSCustomObject]@{
                Device = ($m.DeviceName -replace '^[.\\]+', '')
                Primary = if ($m.Primary) { 'Yes' } else { '' }
                Resolution = $m.Resolution
                Serial = $m.SerialNumber
                Mfg = $m.Manufacturer
                Model = $m.Model
            }
        }
        $monitorTable | Format-Table -AutoSize
    }

    # Memory
    Write-Host "[Memory]" -ForegroundColor Yellow
    if ($mem.Modules -and $mem.Modules.Count -gt 0) {
        $mem.Modules | Format-Table Manufacturer, BankLabel, PartNumber, Speed, CapacityGB -AutoSize
        $summary = "Total: {0} | Used: {1} | Free: {2} | Modules Total: {3} GB" -f `
            ($mem.Total ?? 'n/a'),
            ($mem.Used ?? 'n/a'),
            ($mem.Free ?? 'n/a'),
            ($mem.TotalModuleGB ?? 'n/a')
        Write-Host ("-" * 78)
        Write-Host $summary
    } else {
        $memObj = [PSCustomObject]@{
            Total = $mem.Total
            Used = $mem.Used
            Free = $mem.Free
            PhysicalRAM = $mem.PhysicalRAM
        }
        $memObj | Format-Table -AutoSize
    }

    Write-Host ("-" * 78)

    # Hard Drives Section
    Write-Host
    Write-Host "[Hard Drives]" -ForegroundColor Yellow
    try {
        $drives = Get-PhysicalDisk | Select-Object DeviceId, Model, MediaType, BusType, @{Name="SizeGB"; Expression={ [int]($_.Size/1Gb) }}
        if ($drives -and $drives.Count -gt 0) {
            $drives | Sort-Object DeviceId | Format-Table DeviceId, Model, MediaType, BusType, SizeGB -AutoSize
        } else {
            Write-Host "(No physical disks found)"
        }
    } catch {
        Write-Host "(Error retrieving physical disk info)"
    }

    Write-Host ("-" * 78)
    Write-Host "Press [Enter] to exit" -ForegroundColor Cyan
    Read-Host | Out-Null
}

function Get-HWTerminalSnapshot {
    [CmdletBinding()]
    param()
    try {
        $cpu = Get-CpuInfo
    } catch { $cpu = $null }
    try {
        $mb = Get-MotherboardInfo
    } catch { $mb = $null }
    try {
        $gpuInfo = Get-GpuInfo
        $gpu = $gpuInfo.GPUs
        $monitors = $gpuInfo.Monitors
    } catch { $gpu = @(); $monitors = @() }
    try {
        $mem = Try-GetCim -Class Win32_OperatingSystem
        $phys = Try-GetCim -Class Win32_ComputerSystem
        $memModules = Get-CimInstance Win32_PhysicalMemory
        $memObj = $null
        $modules = @()
        $totalModuleGB = 0
        if ($memModules) {
            foreach ($mod in $memModules) {
                $gb = [math]::Round($mod.Capacity / 1GB, 2)
                $totalModuleGB += $gb
                $modules += [pscustomobject]@{
                    Manufacturer = $mod.Manufacturer
                    BankLabel    = $mod.BankLabel
                    PartNumber   = $mod.PartNumber
                    Speed        = $mod.Speed
                    CapacityGB   = $gb
                }
            }
        }
        if ($mem) {
            $total = $mem.TotalVisibleMemorySize * 1KB
            $free = $mem.FreePhysicalMemory * 1KB
            $memObj = [pscustomobject]@{
                Section     = "Memory"
                Total       = Format-Bytes $total
                Free        = Format-Bytes $free
                Used        = Format-Bytes ($total - $free)
                PhysicalRAM = if ($phys.TotalPhysicalMemory) { Format-Bytes $phys.TotalPhysicalMemory } else { $null }
                Modules     = $modules
                TotalModuleGB = $totalModuleGB
            }
        }
    } catch { $memObj = $null }
    [pscustomobject]@{
        Timestamp   = (Get-Date)
        User        = $env:USERNAME
        CPU         = $cpu
        Motherboard = $mb
        GPU         = $gpu
        Monitors    = $monitors
        Memory      = $memObj
    }
}

function Try-GetCim {
    param(
        [Parameter(Mandatory)][string]$Class,
        [string]$Namespace = 'root\cimv2',
        [hashtable]$PropertyMap = @{}
    )
    try {
        $o = Get-CimInstance -ClassName $Class -Namespace $Namespace -ErrorAction Stop
        if ($PropertyMap.Count -gt 0 -and $o) {
            $sel = [ordered]@{}
            foreach ($k in $PropertyMap.Keys) { $sel[$k] = $o.$($PropertyMap[$k]) }
            return [pscustomobject]$sel
        }
        return $o
    }
    catch {
        Write-Verbose "CIM $Class failed: $($_.Exception.Message)"
        return $null
    }
}

function Try-GetCounter {
    param([Parameter(Mandatory)][string]$Path)
    try {
        (Get-Counter -Counter $Path -ErrorAction Stop).CounterSamples | Select-Object CookedValue
    }
    catch {
        Write-Verbose "Counter $Path failed: $($_.Exception.Message)"
        $null
    }
}

function Format-Bytes {
    param([double]$Bytes)
    $units = "B", "KB", "MB", "GB", "TB"
    $i = 0
    while ($Bytes -ge 1024 -and $i -lt $units.Length - 1) { $Bytes /= 1024; $i++ }
    "{0:N2} {1}" -f $Bytes, $units[$i]
}

function Get-CpuInfo {
    $cpu = Try-GetCim -Class Win32_Processor
    if (-not $cpu) { return [pscustomobject]@{ Section = "CPU"; Error = "Win32_Processor unavailable" } }

    $load = Try-GetCounter '\Processor(_Total)\% Processor Time'
    $mhz = Try-GetCounter '\Processor Information(_Total)\Processor Frequency'

    $tz = Try-GetCim -Class MSAcpi_ThermalZoneTemperature -Namespace root\WMI
    $pkgTemp = $null
    if ($tz) {
        foreach ($t in $tz) {
            if ($t.CurrentTemperature -gt 0) {
                $pkgTemp = [math]::Round(($t.CurrentTemperature / 10 - 273.15), 1)
                break
            }
        }
    }

    $logical = ($cpu | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
    $cores = ($cpu | Measure-Object -Property NumberOfCores -Sum).Sum
    $baseMHz = ($cpu | Select-Object -ExpandProperty MaxClockSpeed | Measure-Object -Average).Average

    $voltage = $cpu.CurrentVoltage
    if ($voltage -is [int] -and $voltage -gt 0) { $voltage = "$voltage (tenths of volt encoded)" } else { $voltage = $null }

    [pscustomobject]@{
        Section             = "CPU"
        Name                = $cpu.Name
        Manufacturer        = $cpu.Manufacturer
        Sockets             = ($cpu | Measure-Object).Count
        Cores               = $cores
        LogicalProcessors   = $logical
        BaseClockMHz        = [math]::Round($baseMHz, 0)
        CurrentClockMHz     = if ($mhz) { [math]::Round($mhz.CookedValue, 0) } else { $null }
        PackageTemperatureC = $pkgTemp
        Voltage             = $voltage
        LoadPercent         = if ($load) { [math]::Round($load.CookedValue, 1) } else { $null }
        L3Cache             = ($cpu.L3CacheSize) ? ("{0} KB" -f $cpu.L3CacheSize) : $null
        Caption             = $cpu.Caption
    }
}

function Get-MotherboardInfo {
    $base = Try-GetCim -Class Win32_BaseBoard
    $bios = Try-GetCim -Class Win32_BIOS
    $chassis = Try-GetCim -Class Win32_SystemEnclosure
    $fans = Try-GetCim -Class Win32_Fan

    $smartTemps = @()
    try {
        $nvme = Get-CimInstance -Namespace root\wmi -Class MSFT_NvmeDriveInformation -ErrorAction Stop
        foreach ($n in $nvme) {
            if ($n.Temperature) { $smartTemps += [pscustomobject]@{ Drive = $n.ModelNumber; TempC = $n.Temperature } }
        }
    }
    catch {}

    [pscustomobject]@{
        Section         = "Motherboard"
        Manufacturer    = $base.Manufacturer
        Product         = $base.Product
        SerialNumber    = $base.SerialNumber
        BIOSVersion     = (@($bios.SMBIOSBIOSVersion) + @($bios.BIOSVersion) | Where-Object { $_ } | ForEach-Object { $_ -is [array] ? ($_.Trim() -join '; ') : $_ } | Where-Object { $_ }) -join " | "
        BIOSReleaseDate = if ($bios.ReleaseDate -is [datetime]) { $bios.ReleaseDate } elseif ($bios.ReleaseDate) { [datetime]::ParseExact(($bios.ReleaseDate -as [string]).Split('.')[0], 'yyyyMMddHHmmss', $null) } else { $null }
        ChassisType     = ($chassis.ChassisTypes -join ',')
        FansDetected    = if ($fans) { ($fans | Measure-Object).Count } else { 0 }
        FanInfo         = if ($fans) { $fans | Select-Object Name, DesiredSpeed, VariableSpeed } else { $null }
        StorageTemps    = if ($smartTemps.Count) { $smartTemps } else { $null }
    }
}

function Get-GpuInfo {
    $gpus = Try-GetCim -Class Win32_VideoController
    $list = @()
    if ($gpus) {
        foreach ($gpu in $gpus) {
            $list += [pscustomobject]@{
                Name              = $gpu.Name
                AdapterRAM        = if ($gpu.AdapterRAM) { Format-Bytes $gpu.AdapterRAM } else { $null }
                DriverVersion     = $gpu.DriverVersion
                DriverDate        = $gpu.DriverDate
                CurrentResolution = if ($gpu.CurrentHorizontalResolution -and $gpu.CurrentVerticalResolution) {
                    "{0}x{1}" -f $gpu.CurrentHorizontalResolution, $gpu.CurrentVerticalResolution
                } else { $null }
                CurrentRefreshHz  = $gpu.CurrentRefreshRate
                VideoProcessor    = $gpu.VideoProcessor
                Status            = $gpu.Status
                DeviceID          = $gpu.DeviceID
                PNPDeviceID       = $gpu.PNPDeviceID
                AdapterCompatibility = $gpu.AdapterCompatibility
            }
        }
    }
    $monitors = @()
    try {
        Add-Type -AssemblyName System.Windows.Forms
        $screens = [System.Windows.Forms.Screen]::AllScreens
        $wmiMonitors = Get-CimInstance -ClassName Win32_DesktopMonitor
        $pnpMonitors = Get-CimInstance -ClassName Win32_PnPEntity | Where-Object { $_.PNPClass -eq 'Monitor' }
        $wmiMonitorIDs = Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID
        function Decode-WmiString($arr) {
            if (-not $arr) { return $null }
            -join ($arr | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ })
        }
        $monitorIdList = @()
        foreach ($id in $wmiMonitorIDs) {
            $monitorIdList += [pscustomobject]@{
                Manufacturer = Decode-WmiString $id.ManufacturerName
                Model        = Decode-WmiString $id.UserFriendlyName
                SerialNumber = Decode-WmiString $id.SerialNumberID
                InstanceName = $id.InstanceName
            }
        }
        $i = 0
        foreach ($s in $screens) {
            $devName = $s.DeviceName
            $monId = $null
            if ($monitorIdList.Count -gt $i) { $monId = $monitorIdList[$i] }
            $wmi = $wmiMonitors | Where-Object { $_.PNPDeviceID -and $devName -replace '^.*?([A-Z0-9]+)$', '$1' -like $_.PNPDeviceID -replace '\\', '' }
            if (-not $wmi) { $wmi = $null }
            $pnp = $pnpMonitors | Where-Object { $_.Name -and $devName -replace '^.*?([A-Z0-9]+)$', '$1' -like $_.Name }
            if (-not $pnp) { $pnp = $null }
            $monitors += [pscustomobject]@{
                DeviceName   = $s.DeviceName
                Primary      = $s.Primary
                Bounds       = "{0}x{1}+{2}+{3}" -f $s.Bounds.Width, $s.Bounds.Height, $s.Bounds.X, $s.Bounds.Y
                Resolution   = "{0}x{1}" -f $s.Bounds.Width, $s.Bounds.Height
                WorkingArea  = "{0}x{1}+{2}+{3}" -f $s.WorkingArea.Width, $s.WorkingArea.Height, $s.WorkingArea.X, $s.WorkingArea.Y
                SerialNumber = $monId.SerialNumber ?? ($wmi.SerialNumber | Select-Object -First 1)
                Manufacturer = $monId.Manufacturer ?? ($wmi.MonitorManufacturer | Select-Object -First 1)
                Model        = $monId.Model ?? ($wmi.Name | Select-Object -First 1)
                PnPName      = $pnp.Name | Select-Object -First 1
            }
            $i++
        }
    }
    catch {
        $monitors = $null
    }
    return @{ GPUs = $list; Monitors = $monitors }
}

function Start-PSHWinfo {
    [CmdletBinding()]
    param()
    Clear-Host
    Write-Host "Starting PowerShell Utility that was inspired by HWinfo64..." -ForegroundColor Cyan
    $shot = $null
    try {
        $shot = Get-HWTerminalSnapshot
    } catch {
        $shot = $null
    }
    if (-not $shot -or -not $shot.CPU) {
        Write-Host "Error: Unable to retrieve hardware information. Please ensure you have the necessary permissions and try again." -ForegroundColor Red
        return
    }
    Clear-Host
    Show-HWTerminalDashboard -Snapshot $shot
}

Export-ModuleMember -Function Start-PSHWinfo