From 9a175df5105e40deadd83fb8256256a87da133b5 Mon Sep 17 00:00:00 2001 From: Yu Leng Date: Thu, 11 Dec 2025 12:46:15 +0800 Subject: [PATCH] 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. --- .../Drivers/WMI/WmiController.cs | 105 ++++-------------- .../PowerDisplay.Lib/Helpers/PnpIdHelper.cs | 86 ++++++++++++++ .../{ => PowerDisplay}/PowerDisplay.ico | Bin .../PowerDisplay/PowerDisplay.csproj | 4 +- .../ViewModels/MainViewModel.Monitors.cs | 12 +- .../PowerDisplay/ViewModels/MainViewModel.cs | 4 +- 6 files changed, 119 insertions(+), 92 deletions(-) create mode 100644 src/modules/powerdisplay/PowerDisplay.Lib/Helpers/PnpIdHelper.cs rename src/modules/powerdisplay/PowerDisplay/Assets/{ => PowerDisplay}/PowerDisplay.ico (100%) diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs index aeb8ea521f..d59dee081f 100644 --- a/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Drivers/WMI/WmiController.cs @@ -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 } /// - /// 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". /// public async Task> 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(); - - foreach (var obj in idResults) - { - try - { - var instanceName = obj.GetPropertyValue("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("InstanceName") ?? string.Empty; var currentBrightness = obj.GetPropertyValue("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); } - /// - /// Get user-friendly name from WMI object. - /// WmiMonitorID returns UserFriendlyName as a fixed-size uint16 array buffer, - /// with UserFriendlyNameLength indicating the actual character count. - /// - private static string? GetUserFriendlyName(WmiObject monitorObject) - { - try - { - var userFriendlyName = monitorObject.GetPropertyValue("UserFriendlyName"); - var nameLength = monitorObject.GetPropertyValue("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 SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default) { diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Helpers/PnpIdHelper.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Helpers/PnpIdHelper.cs new file mode 100644 index 0000000000..7bfba133e4 --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay.Lib/Helpers/PnpIdHelper.cs @@ -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; + +/// +/// 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 +/// +public static class PnpIdHelper +{ + /// + /// Map of common laptop/monitor manufacturer PnP IDs to display names. + /// Only includes manufacturers known to produce laptops with internal displays. + /// + private static readonly FrozenDictionary ManufacturerNames = new Dictionary(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); + + /// + /// Extract the 3-character PnP manufacturer ID from a hardware ID. + /// + /// Hardware ID like "LEN4038" or "BOE0900". + /// The 3-character PnP ID (e.g., "LEN"), or null if invalid. + 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(); + } + + /// + /// Get a user-friendly display name for an internal display based on its hardware ID. + /// + /// Hardware ID like "LEN4038" or "BOE0900". + /// Display name like "Lenovo Built-in Display" or "Built-in Display" as fallback. + 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"; + } +} diff --git a/src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay.ico b/src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay/PowerDisplay.ico similarity index 100% rename from src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay.ico rename to src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay/PowerDisplay.ico diff --git a/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj b/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj index 42ab688fc5..78107c261b 100644 --- a/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj +++ b/src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj @@ -7,7 +7,7 @@ WinExe PowerDisplay app.manifest - Assets\PowerDisplay.ico + Assets\PowerDisplay\PowerDisplay.ico x64;ARM64 true true @@ -89,7 +89,7 @@ - + PreserveNewest diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs index f7b0b1d889..1e602a55c0 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Monitors.cs @@ -58,23 +58,31 @@ public partial class MainViewModel } } - public async Task RefreshMonitorsAsync() + /// + /// Refresh monitors list asynchronously. + /// + /// If true, skip the IsScanning check (used by OnDisplayChanged which sets IsScanning before calling). + 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) diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs index 4b5b41b849..069c23168f 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs @@ -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); } ///