Improve WMI internal display naming and refresh logic

Enhance monitor discovery by extracting manufacturer IDs from WMI and mapping them to user-friendly names using a new PnpIdHelper. Remove unreliable WmiMonitorID name parsing. Add detailed logging and allow forced monitor refreshes during display changes. Update asset paths in project file for better organization.
This commit is contained in:
Yu Leng
2025-12-11 12:46:15 +08:00
parent f07fa4db60
commit 9a175df510
6 changed files with 119 additions and 92 deletions

View File

@@ -9,6 +9,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Common.Helpers;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using WmiLight;
@@ -25,7 +26,6 @@ namespace PowerDisplay.Common.Drivers.WMI
private const string WmiNamespace = @"root\WMI";
private const string BrightnessQueryClass = "WmiMonitorBrightness";
private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
private const string MonitorIdClass = "WmiMonitorID";
// Common WMI error codes for classification
private const int WbemENotFound = unchecked((int)0x80041002);
@@ -244,7 +244,9 @@ namespace PowerDisplay.Common.Drivers.WMI
}
/// <summary>
/// Discover supported monitors
/// Discover supported monitors.
/// WMI brightness control is typically only available on internal laptop displays,
/// which don't have meaningful UserFriendlyName in WmiMonitorID, so we use "Built-in Display".
/// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
@@ -257,8 +259,8 @@ namespace PowerDisplay.Common.Drivers.WMI
{
using var connection = new WmiConnection(WmiNamespace);
// First check if WMI brightness support is available
var brightnessQuery = $"SELECT * FROM {BrightnessQueryClass}";
// Query WMI brightness support - only internal displays typically support this
var brightnessQuery = $"SELECT InstanceName, CurrentBrightness FROM {BrightnessQueryClass}";
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
if (brightnessResults.Count == 0)
@@ -266,37 +268,6 @@ namespace PowerDisplay.Common.Drivers.WMI
return monitors;
}
// Get monitor information
var idQuery = $"SELECT * FROM {MonitorIdClass}";
var idResults = connection.CreateQuery(idQuery).ToList();
var monitorInfos = new Dictionary<string, (string Name, string InstanceName)>();
foreach (var obj in idResults)
{
try
{
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
var userFriendlyName = GetUserFriendlyName(obj);
Logger.LogDebug($"WMI MonitorID: InstanceName='{instanceName}', UserFriendlyName='{userFriendlyName ?? "(null)"}'");
if (string.IsNullOrEmpty(userFriendlyName))
{
userFriendlyName = "Internal Display";
}
if (!string.IsNullOrEmpty(instanceName))
{
monitorInfos[instanceName] = (userFriendlyName, instanceName);
}
}
catch (Exception ex)
{
// Skip problematic entries
Logger.LogDebug($"Failed to parse WMI monitor info: {ex.Message}");
}
}
// Get MonitorDisplayInfo from QueryDisplayConfig - this provides the correct monitor numbers
var monitorDisplayInfos = Drivers.DDC.DdcCiNative.GetAllMonitorDisplayInfo();
@@ -308,31 +279,29 @@ namespace PowerDisplay.Common.Drivers.WMI
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
var name = "Internal Display";
if (monitorInfos.TryGetValue(instanceName, out var info))
{
name = info.Name;
}
// Extract hardware ID from InstanceName
// e.g., "DISPLAY\LEN4038\4&40f4dee&0&UID8388688_0" -> "LEN4038"
var hardwareId = ExtractHardwareIdFromInstanceName(instanceName);
// Extract EdidId from InstanceName
// e.g., "DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0" -> "BOE0900"
var edidId = ExtractHardwareIdFromInstanceName(instanceName);
// Get MonitorDisplayInfo from QueryDisplayConfig by matching EdidId
// Get MonitorDisplayInfo from QueryDisplayConfig by matching hardware ID
// This provides MonitorNumber and GdiDeviceName for display settings APIs
var displayInfo = GetMonitorDisplayInfoByHardwareId(edidId, monitorDisplayInfos);
var displayInfo = GetMonitorDisplayInfoByHardwareId(hardwareId, monitorDisplayInfos);
int monitorNumber = displayInfo?.MonitorNumber ?? 0;
string gdiDeviceName = displayInfo?.GdiDeviceName ?? string.Empty;
// Generate unique monitor Id: "WMI_{EdidId}_{MonitorNumber}"
string monitorId = !string.IsNullOrEmpty(edidId)
? $"WMI_{edidId}_{monitorNumber}"
// Generate unique monitor Id: "WMI_{HardwareId}_{MonitorNumber}"
string monitorId = !string.IsNullOrEmpty(hardwareId)
? $"WMI_{hardwareId}_{monitorNumber}"
: $"WMI_Unknown_{monitorNumber}";
// Get display name from PnP manufacturer ID (e.g., "Lenovo Built-in Display")
var displayName = PnpIdHelper.GetBuiltInDisplayName(hardwareId);
Logger.LogDebug($"WMI: Found internal display '{hardwareId}' -> '{displayName}'");
var monitor = new Monitor
{
Id = monitorId,
Name = name,
Name = displayName,
CurrentBrightness = currentBrightness,
MinBrightness = 0,
MaxBrightness = 100,
@@ -349,14 +318,12 @@ namespace PowerDisplay.Common.Drivers.WMI
}
catch (Exception ex)
{
// Skip problematic monitors
Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
}
}
}
catch (WmiException ex)
{
// Return empty list instead of throwing exception
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
}
catch (Exception ex)
@@ -369,40 +336,6 @@ namespace PowerDisplay.Common.Drivers.WMI
cancellationToken);
}
/// <summary>
/// Get user-friendly name from WMI object.
/// WmiMonitorID returns UserFriendlyName as a fixed-size uint16 array buffer,
/// with UserFriendlyNameLength indicating the actual character count.
/// </summary>
private static string? GetUserFriendlyName(WmiObject monitorObject)
{
try
{
var userFriendlyName = monitorObject.GetPropertyValue<ushort[]>("UserFriendlyName");
var nameLength = monitorObject.GetPropertyValue<ushort>("UserFriendlyNameLength");
if (userFriendlyName != null && nameLength > 0 && nameLength <= userFriendlyName.Length)
{
// Use UserFriendlyNameLength to extract only valid characters
var chars = userFriendlyName
.Take(nameLength)
.Select(c => (char)c)
.ToArray();
if (chars.Length > 0)
{
return new string(chars).Trim();
}
}
}
catch (Exception ex)
{
Logger.LogDebug($"Failed to parse UserFriendlyName: {ex.Message}");
}
return null;
}
// Extended features not supported by WMI
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
{

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
namespace PowerDisplay.Common.Helpers;
/// <summary>
/// Helper class for mapping PnP (Plug and Play) manufacturer IDs to display names.
/// PnP IDs are 3-character codes assigned by Microsoft to hardware manufacturers.
/// See: https://uefi.org/pnp_id_list
/// </summary>
public static class PnpIdHelper
{
/// <summary>
/// Map of common laptop/monitor manufacturer PnP IDs to display names.
/// Only includes manufacturers known to produce laptops with internal displays.
/// </summary>
private static readonly FrozenDictionary<string, string> ManufacturerNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// Major laptop manufacturers
{ "ACR", "Acer" },
{ "AUO", "AU Optronics" },
{ "BOE", "BOE" },
{ "CMN", "Chi Mei Innolux" },
{ "DEL", "Dell" },
{ "HWP", "HP" },
{ "IVO", "InfoVision" },
{ "LEN", "Lenovo" },
{ "LGD", "LG Display" },
{ "NCP", "Najing CEC Panda" },
{ "SAM", "Samsung" },
{ "SDC", "Samsung Display" },
{ "SEC", "Samsung Electronics" },
{ "SHP", "Sharp" },
{ "AUS", "ASUS" },
{ "MSI", "MSI" },
{ "APP", "Apple" },
{ "SNY", "Sony" },
{ "PHL", "Philips" },
{ "HSD", "HannStar" },
{ "CPT", "Chunghwa Picture Tubes" },
{ "QDS", "Quanta Display" },
{ "TMX", "Tianma Microelectronics" },
{ "CSO", "CSOT" },
// Microsoft Surface
{ "MSF", "Microsoft" },
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Extract the 3-character PnP manufacturer ID from a hardware ID.
/// </summary>
/// <param name="hardwareId">Hardware ID like "LEN4038" or "BOE0900".</param>
/// <returns>The 3-character PnP ID (e.g., "LEN"), or null if invalid.</returns>
public static string? ExtractPnpId(string? hardwareId)
{
if (string.IsNullOrEmpty(hardwareId) || hardwareId.Length < 3)
{
return null;
}
// PnP ID is the first 3 characters
return hardwareId.Substring(0, 3).ToUpperInvariant();
}
/// <summary>
/// Get a user-friendly display name for an internal display based on its hardware ID.
/// </summary>
/// <param name="hardwareId">Hardware ID like "LEN4038" or "BOE0900".</param>
/// <returns>Display name like "Lenovo Built-in Display" or "Built-in Display" as fallback.</returns>
public static string GetBuiltInDisplayName(string? hardwareId)
{
var pnpId = ExtractPnpId(hardwareId);
if (pnpId != null && ManufacturerNames.TryGetValue(pnpId, out var manufacturer))
{
return $"{manufacturer} Built-in Display";
}
return "Built-in Display";
}
}

View File

Before

Width:  |  Height:  |  Size: 468 KiB

After

Width:  |  Height:  |  Size: 468 KiB

View File

@@ -7,7 +7,7 @@
<OutputType>WinExe</OutputType>
<RootNamespace>PowerDisplay</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\PowerDisplay.ico</ApplicationIcon>
<ApplicationIcon>Assets\PowerDisplay\PowerDisplay.ico</ApplicationIcon>
<Platforms>x64;ARM64</Platforms>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
@@ -89,7 +89,7 @@
</ItemGroup>
<!-- Copy Assets folder to output directory -->
<ItemGroup>
<Content Include="Assets\**\*">
<Content Include="Assets\PowerDisplay\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

View File

@@ -58,23 +58,31 @@ public partial class MainViewModel
}
}
public async Task RefreshMonitorsAsync()
/// <summary>
/// Refresh monitors list asynchronously.
/// </summary>
/// <param name="skipScanningCheck">If true, skip the IsScanning check (used by OnDisplayChanged which sets IsScanning before calling).</param>
public async Task RefreshMonitorsAsync(bool skipScanningCheck = false)
{
if (IsScanning)
if (!skipScanningCheck && IsScanning)
{
Logger.LogDebug("[RefreshMonitorsAsync] Skipping refresh - already scanning");
return;
}
try
{
IsScanning = true;
Logger.LogInfo("[RefreshMonitorsAsync] Starting monitor discovery...");
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
Logger.LogInfo($"[RefreshMonitorsAsync] Discovery complete, found {monitors.Count} monitors");
_dispatcherQueue.TryEnqueue(() =>
{
UpdateMonitorList(monitors, isInitialLoad: false);
IsScanning = false;
Logger.LogInfo("[RefreshMonitorsAsync] UI update complete, scanning stopped");
});
}
catch (Exception ex)

View File

@@ -341,9 +341,9 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Wait for hardware to stabilize (DDC/CI may not be ready immediately after plug)
await Task.Delay(TimeSpan.FromSeconds(5));
// Perform actual refresh
// Perform actual refresh - skip scanning check since we already set IsScanning above
Logger.LogInfo("[MainViewModel] Delay complete, now refreshing monitors...");
await RefreshMonitorsAsync();
await RefreshMonitorsAsync(skipScanningCheck: true);
}
/// <summary>