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;
using System.Threading.Tasks; using System.Threading.Tasks;
using ManagedCommon; using ManagedCommon;
using PowerDisplay.Common.Helpers;
using PowerDisplay.Common.Interfaces; using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models; using PowerDisplay.Common.Models;
using WmiLight; using WmiLight;
@@ -25,7 +26,6 @@ namespace PowerDisplay.Common.Drivers.WMI
private const string WmiNamespace = @"root\WMI"; private const string WmiNamespace = @"root\WMI";
private const string BrightnessQueryClass = "WmiMonitorBrightness"; private const string BrightnessQueryClass = "WmiMonitorBrightness";
private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods"; private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
private const string MonitorIdClass = "WmiMonitorID";
// Common WMI error codes for classification // Common WMI error codes for classification
private const int WbemENotFound = unchecked((int)0x80041002); private const int WbemENotFound = unchecked((int)0x80041002);
@@ -244,7 +244,9 @@ namespace PowerDisplay.Common.Drivers.WMI
} }
/// <summary> /// <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> /// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default) public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{ {
@@ -257,8 +259,8 @@ namespace PowerDisplay.Common.Drivers.WMI
{ {
using var connection = new WmiConnection(WmiNamespace); using var connection = new WmiConnection(WmiNamespace);
// First check if WMI brightness support is available // Query WMI brightness support - only internal displays typically support this
var brightnessQuery = $"SELECT * FROM {BrightnessQueryClass}"; var brightnessQuery = $"SELECT InstanceName, CurrentBrightness FROM {BrightnessQueryClass}";
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList(); var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
if (brightnessResults.Count == 0) if (brightnessResults.Count == 0)
@@ -266,37 +268,6 @@ namespace PowerDisplay.Common.Drivers.WMI
return monitors; 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 // Get MonitorDisplayInfo from QueryDisplayConfig - this provides the correct monitor numbers
var monitorDisplayInfos = Drivers.DDC.DdcCiNative.GetAllMonitorDisplayInfo(); var monitorDisplayInfos = Drivers.DDC.DdcCiNative.GetAllMonitorDisplayInfo();
@@ -308,31 +279,29 @@ namespace PowerDisplay.Common.Drivers.WMI
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty; var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness"); var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
var name = "Internal Display"; // Extract hardware ID from InstanceName
if (monitorInfos.TryGetValue(instanceName, out var info)) // e.g., "DISPLAY\LEN4038\4&40f4dee&0&UID8388688_0" -> "LEN4038"
{ var hardwareId = ExtractHardwareIdFromInstanceName(instanceName);
name = info.Name;
}
// Extract EdidId from InstanceName // Get MonitorDisplayInfo from QueryDisplayConfig by matching hardware ID
// e.g., "DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0" -> "BOE0900"
var edidId = ExtractHardwareIdFromInstanceName(instanceName);
// Get MonitorDisplayInfo from QueryDisplayConfig by matching EdidId
// This provides MonitorNumber and GdiDeviceName for display settings APIs // This provides MonitorNumber and GdiDeviceName for display settings APIs
var displayInfo = GetMonitorDisplayInfoByHardwareId(edidId, monitorDisplayInfos); var displayInfo = GetMonitorDisplayInfoByHardwareId(hardwareId, monitorDisplayInfos);
int monitorNumber = displayInfo?.MonitorNumber ?? 0; int monitorNumber = displayInfo?.MonitorNumber ?? 0;
string gdiDeviceName = displayInfo?.GdiDeviceName ?? string.Empty; string gdiDeviceName = displayInfo?.GdiDeviceName ?? string.Empty;
// Generate unique monitor Id: "WMI_{EdidId}_{MonitorNumber}" // Generate unique monitor Id: "WMI_{HardwareId}_{MonitorNumber}"
string monitorId = !string.IsNullOrEmpty(edidId) string monitorId = !string.IsNullOrEmpty(hardwareId)
? $"WMI_{edidId}_{monitorNumber}" ? $"WMI_{hardwareId}_{monitorNumber}"
: $"WMI_Unknown_{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 var monitor = new Monitor
{ {
Id = monitorId, Id = monitorId,
Name = name, Name = displayName,
CurrentBrightness = currentBrightness, CurrentBrightness = currentBrightness,
MinBrightness = 0, MinBrightness = 0,
MaxBrightness = 100, MaxBrightness = 100,
@@ -349,14 +318,12 @@ namespace PowerDisplay.Common.Drivers.WMI
} }
catch (Exception ex) catch (Exception ex)
{ {
// Skip problematic monitors
Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}"); Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
} }
} }
} }
catch (WmiException ex) catch (WmiException ex)
{ {
// Return empty list instead of throwing exception
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})"); Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
} }
catch (Exception ex) catch (Exception ex)
@@ -369,40 +336,6 @@ namespace PowerDisplay.Common.Drivers.WMI
cancellationToken); 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 // Extended features not supported by WMI
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default) 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> <OutputType>WinExe</OutputType>
<RootNamespace>PowerDisplay</RootNamespace> <RootNamespace>PowerDisplay</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\PowerDisplay.ico</ApplicationIcon> <ApplicationIcon>Assets\PowerDisplay\PowerDisplay.ico</ApplicationIcon>
<Platforms>x64;ARM64</Platforms> <Platforms>x64;ARM64</Platforms>
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling> <EnableMsixTooling>true</EnableMsixTooling>
@@ -89,7 +89,7 @@
</ItemGroup> </ItemGroup>
<!-- Copy Assets folder to output directory --> <!-- Copy Assets folder to output directory -->
<ItemGroup> <ItemGroup>
<Content Include="Assets\**\*"> <Content Include="Assets\PowerDisplay\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </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; return;
} }
try try
{ {
IsScanning = true; IsScanning = true;
Logger.LogInfo("[RefreshMonitorsAsync] Starting monitor discovery...");
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token); var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
Logger.LogInfo($"[RefreshMonitorsAsync] Discovery complete, found {monitors.Count} monitors");
_dispatcherQueue.TryEnqueue(() => _dispatcherQueue.TryEnqueue(() =>
{ {
UpdateMonitorList(monitors, isInitialLoad: false); UpdateMonitorList(monitors, isInitialLoad: false);
IsScanning = false; IsScanning = false;
Logger.LogInfo("[RefreshMonitorsAsync] UI update complete, scanning stopped");
}); });
} }
catch (Exception ex) 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) // Wait for hardware to stabilize (DDC/CI may not be ready immediately after plug)
await Task.Delay(TimeSpan.FromSeconds(5)); 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..."); Logger.LogInfo("[MainViewModel] Delay complete, now refreshing monitors...");
await RefreshMonitorsAsync(); await RefreshMonitorsAsync(skipScanningCheck: true);
} }
/// <summary> /// <summary>