2025-10-20 16:22:47 +08:00
// 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.Generic ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using ManagedCommon ;
2025-11-24 18:08:11 +08:00
using PowerDisplay.Common.Interfaces ;
using PowerDisplay.Common.Models ;
using PowerDisplay.Common.Utils ;
2025-11-24 21:58:34 +08:00
using static PowerDisplay . Common . Drivers . NativeConstants ;
using static PowerDisplay . Common . Drivers . NativeDelegates ;
using static PowerDisplay . Common . Drivers . PInvoke ;
2025-11-24 18:08:11 +08:00
using Monitor = PowerDisplay . Common . Models . Monitor ;
2025-10-20 16:22:47 +08:00
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
2025-11-24 21:58:34 +08:00
using MONITORINFOEX = PowerDisplay . Common . Drivers . MonitorInfoEx ;
using PHYSICAL_MONITOR = PowerDisplay . Common . Drivers . PhysicalMonitor ;
using RECT = PowerDisplay . Common . Drivers . Rect ;
2025-10-20 16:22:47 +08:00
2025-11-24 21:58:34 +08:00
namespace PowerDisplay.Common.Drivers.DDC
2025-10-20 16:22:47 +08:00
{
/// <summary>
/// DDC/CI monitor controller for controlling external monitors
/// </summary>
public partial class DdcCiController : IMonitorController , IDisposable
{
2025-12-10 06:21:50 +08:00
/// <summary>
/// Represents a candidate monitor discovered during Phase 1 of monitor enumeration.
/// </summary>
/// <param name="Handle">Physical monitor handle for DDC/CI communication</param>
/// <param name="PhysicalMonitor">Native physical monitor structure with description</param>
2025-12-10 06:47:39 +08:00
/// <param name="MonitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
2025-12-10 06:21:50 +08:00
private readonly record struct CandidateMonitor (
IntPtr Handle ,
PHYSICAL_MONITOR PhysicalMonitor ,
2025-12-10 06:47:39 +08:00
MonitorDisplayInfo MonitorInfo ) ;
2025-12-10 06:21:50 +08:00
2025-11-24 18:08:11 +08:00
/// <summary>
/// Delay between retry attempts for DDC/CI operations (in milliseconds)
/// </summary>
private const int RetryDelayMs = 100 ;
2025-10-20 16:22:47 +08:00
private readonly PhysicalMonitorHandleManager _handleManager = new ( ) ;
private readonly MonitorDiscoveryHelper _discoveryHelper ;
private bool _disposed ;
public DdcCiController ( )
{
2025-11-14 13:17:55 +08:00
_discoveryHelper = new MonitorDiscoveryHelper ( ) ;
2025-10-20 16:22:47 +08:00
}
public string Name = > "DDC/CI Monitor Controller" ;
/// <summary>
2025-11-27 17:34:44 +08:00
/// Check if the specified monitor can be controlled.
2025-11-27 17:41:32 +08:00
/// Uses quick connection check since capabilities are already cached during discovery.
2025-10-20 16:22:47 +08:00
/// </summary>
public async Task < bool > CanControlMonitorAsync ( Monitor monitor , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-10-20 16:22:47 +08:00
return await Task . Run (
( ) = >
{
var physicalHandle = GetPhysicalHandle ( monitor ) ;
2025-11-27 17:34:44 +08:00
if ( physicalHandle = = IntPtr . Zero )
{
return false ;
}
2025-11-27 17:41:32 +08:00
// Capabilities are always cached during DiscoverMonitorsAsync Phase 2,
// so we can use quick connection check instead of full validation
return DdcCiNative . QuickConnectionCheck ( physicalHandle ) ;
2025-10-20 16:22:47 +08:00
} ,
cancellationToken ) ;
}
/// <summary>
2025-11-14 13:17:55 +08:00
/// Get monitor brightness using VCP code 0x10
2025-10-20 16:22:47 +08:00
/// </summary>
public async Task < BrightnessInfo > GetBrightnessAsync ( Monitor monitor , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-10-20 16:22:47 +08:00
return await Task . Run (
( ) = >
{
var physicalHandle = GetPhysicalHandle ( monitor ) ;
2025-11-26 05:02:49 +08:00
var result = GetBrightnessInfoCore ( monitor . Id , physicalHandle ) ;
2025-10-20 16:22:47 +08:00
2025-11-26 05:02:49 +08:00
if ( ! result . IsValid )
2025-10-20 16:22:47 +08:00
{
2025-11-26 05:02:49 +08:00
Logger . LogWarning ( $"[{monitor.Id}] Failed to read brightness" ) ;
2025-10-20 16:22:47 +08:00
}
2025-11-26 05:02:49 +08:00
return result ;
2025-10-20 16:22:47 +08:00
} ,
cancellationToken ) ;
}
/// <summary>
2025-11-14 13:17:55 +08:00
/// Set monitor brightness using VCP code 0x10
2025-10-20 16:22:47 +08:00
/// </summary>
2025-12-04 05:44:59 +08:00
public Task < MonitorOperationResult > SetBrightnessAsync ( Monitor monitor , int brightness , CancellationToken cancellationToken = default )
= > SetVcpFeatureAsync ( monitor , NativeConstants . VcpCodeBrightness , brightness , 0 , 100 , cancellationToken ) ;
2025-10-20 16:22:47 +08:00
/// <summary>
/// Set monitor contrast
/// </summary>
public Task < MonitorOperationResult > SetContrastAsync ( Monitor monitor , int contrast , CancellationToken cancellationToken = default )
= > SetVcpFeatureAsync ( monitor , NativeConstants . VcpCodeContrast , contrast , 0 , 100 , cancellationToken ) ;
/// <summary>
/// Set monitor volume
/// </summary>
public Task < MonitorOperationResult > SetVolumeAsync ( Monitor monitor , int volume , CancellationToken cancellationToken = default )
= > SetVcpFeatureAsync ( monitor , NativeConstants . VcpCodeVolume , volume , 0 , 100 , cancellationToken ) ;
/// <summary>
2025-11-14 13:17:55 +08:00
/// Get monitor color temperature using VCP code 0x14 (Select Color Preset)
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
2025-10-20 16:22:47 +08:00
/// </summary>
public async Task < BrightnessInfo > GetColorTemperatureAsync ( Monitor monitor , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-12-10 06:21:50 +08:00
return await GetVcpFeatureAsync ( monitor , VcpCodeSelectColorPreset , "Color temperature" , cancellationToken ) ;
2025-10-20 16:22:47 +08:00
}
/// <summary>
2025-11-14 13:17:55 +08:00
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
2025-10-20 16:22:47 +08:00
/// </summary>
2025-11-14 13:17:55 +08:00
/// <param name="monitor">Monitor to control</param>
/// <param name="colorTemperature">VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature</param>
/// <param name="cancellationToken">Cancellation token</param>
2025-10-20 16:22:47 +08:00
public async Task < MonitorOperationResult > SetColorTemperatureAsync ( Monitor monitor , int colorTemperature , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-10-20 16:22:47 +08:00
return await Task . Run (
( ) = >
{
if ( monitor . Handle = = IntPtr . Zero )
{
return MonitorOperationResult . Failure ( "Invalid monitor handle" ) ;
}
try
{
2025-12-10 06:21:50 +08:00
// Validate value is in supported list
var validationError = ValidateDiscreteVcpValue ( monitor , VcpCodeSelectColorPreset , colorTemperature , "Color preset" ) ;
if ( validationError ! = null )
2025-10-20 16:22:47 +08:00
{
2025-12-10 06:21:50 +08:00
return validationError . Value ;
2025-10-20 16:22:47 +08:00
}
2025-11-14 13:17:55 +08:00
// Set VCP 0x14 value
2025-12-10 06:21:50 +08:00
var presetName = VcpValueNames . GetFormattedName ( VcpCodeSelectColorPreset , colorTemperature ) ;
2025-11-14 13:17:55 +08:00
if ( DdcCiNative . TrySetVCPFeature ( monitor . Handle , VcpCodeSelectColorPreset , ( uint ) colorTemperature ) )
2025-10-20 16:22:47 +08:00
{
Refactor PowerDisplay for dynamic monitor capabilities
Removed reliance on static `MonitorType` enumeration, replacing it with dynamic `CommunicationMethod` for better flexibility. Updated `IMonitorController` and `MonitorManager` to dynamically determine monitor control capabilities.
Refactored `Monitor` model to streamline properties and improve color temperature handling. Enhanced `MonitorViewModel` with unified methods for brightness, contrast, volume, and color temperature updates, improving UI responsiveness and hardware synchronization.
Improved settings handling by adding support for hidden monitors, preserving user preferences, and separating UI configuration from hardware parameter updates. Updated the PowerDisplay Settings UI with warnings, confirmation dialogs, and better VCP capabilities formatting.
Removed legacy IPC code in favor of event-driven settings updates. Conducted general code cleanup, improving logging, error handling, and documentation for maintainability.
2025-11-14 16:45:22 +08:00
Logger . LogInfo ( $"[{monitor.Id}] Set color temperature to {presetName} via 0x14" ) ;
2025-10-20 16:22:47 +08:00
return MonitorOperationResult . Success ( ) ;
}
var lastError = GetLastError ( ) ;
2025-11-14 13:17:55 +08:00
Logger . LogError ( $"[{monitor.Id}] Failed to set color temperature, error: {lastError}" ) ;
2025-12-10 06:21:50 +08:00
return MonitorOperationResult . Failure ( "Failed to set color temperature via DDC/CI" , ( int ) lastError ) ;
2025-10-20 16:22:47 +08:00
}
catch ( Exception ex )
{
2025-11-14 13:17:55 +08:00
Logger . LogError ( $"[{monitor.Id}] Exception setting color temperature: {ex.Message}" ) ;
2025-10-20 16:22:47 +08:00
return MonitorOperationResult . Failure ( $"Exception setting color temperature: {ex.Message}" ) ;
}
} ,
cancellationToken ) ;
}
2025-11-27 14:51:31 +08:00
/// <summary>
/// Get current input source using VCP code 0x60
/// Returns the raw VCP value (e.g., 0x11 for HDMI-1)
/// </summary>
public async Task < BrightnessInfo > GetInputSourceAsync ( Monitor monitor , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-12-10 06:21:50 +08:00
return await GetVcpFeatureAsync ( monitor , VcpCodeInputSource , "Input source" , cancellationToken ) ;
2025-11-27 14:51:31 +08:00
}
/// <summary>
/// Set input source using VCP code 0x60
/// </summary>
/// <param name="monitor">Monitor to control</param>
/// <param name="inputSource">VCP input source value (e.g., 0x11 for HDMI-1)</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task < MonitorOperationResult > SetInputSourceAsync ( Monitor monitor , int inputSource , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-11-27 14:51:31 +08:00
return await Task . Run (
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
async ( ) = >
2025-11-27 14:51:31 +08:00
{
if ( monitor . Handle = = IntPtr . Zero )
{
return MonitorOperationResult . Failure ( "Invalid monitor handle" ) ;
}
try
{
2025-12-10 06:21:50 +08:00
// Validate value is in supported list
var validationError = ValidateDiscreteVcpValue ( monitor , VcpCodeInputSource , inputSource , "Input source" ) ;
if ( validationError ! = null )
2025-11-27 14:51:31 +08:00
{
2025-12-10 06:21:50 +08:00
return validationError . Value ;
2025-11-27 14:51:31 +08:00
}
// Set VCP 0x60 value
2025-12-10 06:21:50 +08:00
var sourceName = VcpValueNames . GetFormattedName ( VcpCodeInputSource , inputSource ) ;
2025-11-27 14:51:31 +08:00
if ( DdcCiNative . TrySetVCPFeature ( monitor . Handle , VcpCodeInputSource , ( uint ) inputSource ) )
{
Logger . LogInfo ( $"[{monitor.Id}] Set input source to {sourceName} via 0x60" ) ;
// Verify the change by reading back the value after a short delay
2025-12-10 06:21:50 +08:00
await VerifyInputSourceChangeAsync ( monitor , inputSource , cancellationToken ) ;
2025-11-27 14:51:31 +08:00
// Update the monitor model with the new value
monitor . CurrentInputSource = inputSource ;
return MonitorOperationResult . Success ( ) ;
}
var lastError = GetLastError ( ) ;
Logger . LogError ( $"[{monitor.Id}] Failed to set input source, error: {lastError}" ) ;
2025-12-10 06:21:50 +08:00
return MonitorOperationResult . Failure ( "Failed to set input source via DDC/CI" , ( int ) lastError ) ;
2025-11-27 14:51:31 +08:00
}
catch ( Exception ex )
{
Logger . LogError ( $"[{monitor.Id}] Exception setting input source: {ex.Message}" ) ;
return MonitorOperationResult . Failure ( $"Exception setting input source: {ex.Message}" ) ;
}
} ,
cancellationToken ) ;
}
2025-12-10 06:21:50 +08:00
/// <summary>
/// Verify input source change by reading back the value after a short delay.
/// Logs warning if verification fails or value doesn't match.
/// </summary>
private static async Task VerifyInputSourceChangeAsync ( Monitor monitor , int expectedValue , CancellationToken cancellationToken )
{
await Task . Delay ( 100 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( DdcCiNative . TryGetVCPFeature ( monitor . Handle , VcpCodeInputSource , out uint verifyValue , out uint _ ) )
{
var verifyName = VcpValueNames . GetFormattedName ( VcpCodeInputSource , ( int ) verifyValue ) ;
if ( verifyValue = = ( uint ) expectedValue )
{
Logger . LogDebug ( $"[{monitor.Id}] Input source verified: {verifyName} (0x{verifyValue:X2})" ) ;
}
else
{
Logger . LogWarning ( $"[{monitor.Id}] Input source verification mismatch! Expected 0x{expectedValue:X2}, got {verifyName} (0x{verifyValue:X2}). Monitor may have refused to switch (no signal on target port?)" ) ;
}
}
else
{
Logger . LogWarning ( $"[{monitor.Id}] Could not verify input source change" ) ;
}
}
2025-10-20 16:22:47 +08:00
/// <summary>
2025-11-27 17:34:44 +08:00
/// Get monitor capabilities string with retry logic.
/// Uses cached CapabilitiesRaw if available to avoid slow I2C operations.
2025-10-20 16:22:47 +08:00
/// </summary>
public async Task < string > GetCapabilitiesStringAsync ( Monitor monitor , CancellationToken cancellationToken = default )
{
Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).
Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.
Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.
Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.
Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.
Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
ArgumentNullException . ThrowIfNull ( monitor ) ;
2025-11-27 17:34:44 +08:00
// Check if capabilities are already cached
if ( ! string . IsNullOrEmpty ( monitor . CapabilitiesRaw ) )
{
Logger . LogDebug ( $"GetCapabilitiesStringAsync: Using cached capabilities for {monitor.Id} (length: {monitor.CapabilitiesRaw.Length})" ) ;
return monitor . CapabilitiesRaw ;
}
2025-10-20 16:22:47 +08:00
return await Task . Run (
( ) = >
{
2025-11-26 05:02:49 +08:00
if ( monitor . Handle = = IntPtr . Zero )
2025-11-14 02:51:43 +08:00
{
return string . Empty ;
}
2025-11-26 05:02:49 +08:00
try
2025-10-20 16:22:47 +08:00
{
2025-11-26 05:02:49 +08:00
// Step 1: Get capabilities string length with retry
var length = RetryHelper . ExecuteWithRetry (
( ) = >
2025-10-20 16:22:47 +08:00
{
2025-11-26 05:02:49 +08:00
if ( GetCapabilitiesStringLength ( monitor . Handle , out uint len ) & & len > 0 )
2025-11-14 02:51:43 +08:00
{
2025-11-26 05:02:49 +08:00
return len ;
2025-11-14 02:51:43 +08:00
}
2025-11-26 05:02:49 +08:00
return 0 u ;
} ,
len = > len > 0 ,
maxRetries : 3 ,
delayMs : RetryDelayMs ,
operationName : "GetCapabilitiesStringLength" ) ;
if ( length = = 0 )
2025-10-20 16:22:47 +08:00
{
2025-11-26 05:02:49 +08:00
return string . Empty ;
2025-10-20 16:22:47 +08:00
}
2025-11-14 02:51:43 +08:00
2025-11-26 05:02:49 +08:00
// Step 2: Get actual capabilities string with retry
var capsString = RetryHelper . ExecuteWithRetry (
( ) = > TryGetCapabilitiesString ( monitor . Handle , length ) ,
str = > ! string . IsNullOrEmpty ( str ) ,
maxRetries : 5 ,
delayMs : RetryDelayMs ,
operationName : "GetCapabilitiesString" ) ;
if ( ! string . IsNullOrEmpty ( capsString ) )
2025-11-14 02:51:43 +08:00
{
2025-12-01 06:09:26 +08:00
Logger . LogDebug ( $"Got capabilities string (length: {capsString.Length})" ) ;
2025-11-26 05:02:49 +08:00
return capsString ;
2025-11-14 02:51:43 +08:00
}
2025-10-20 16:22:47 +08:00
}
2025-11-26 05:02:49 +08:00
catch ( Exception ex )
{
Logger . LogError ( $"Exception getting capabilities string: {ex.Message}" ) ;
}
2025-11-14 02:51:43 +08:00
2025-11-26 05:02:49 +08:00
return string . Empty ;
} ,
cancellationToken ) ;
}
/// <summary>
/// Try to get capabilities string from monitor handle.
/// </summary>
private string? TryGetCapabilitiesString ( IntPtr handle , uint length )
{
var buffer = System . Runtime . InteropServices . Marshal . AllocHGlobal ( ( int ) length ) ;
try
{
if ( CapabilitiesRequestAndCapabilitiesReply ( handle , buffer , length ) )
2025-10-20 16:22:47 +08:00
{
2025-11-26 05:02:49 +08:00
return System . Runtime . InteropServices . Marshal . PtrToStringAnsi ( buffer ) ;
2025-10-20 16:22:47 +08:00
}
2025-11-26 05:02:49 +08:00
return null ;
}
finally
{
System . Runtime . InteropServices . Marshal . FreeHGlobal ( buffer ) ;
}
2025-10-20 16:22:47 +08:00
}
/// <summary>
2025-12-10 06:21:50 +08:00
/// Discover supported monitors using a three-phase approach:
/// Phase 1: Enumerate and collect candidate monitors with their handles
/// Phase 2: Fetch DDC/CI capabilities in parallel (slow I2C operations)
/// Phase 3: Create Monitor objects for valid DDC/CI monitors
2025-10-20 16:22:47 +08:00
/// </summary>
public async Task < IEnumerable < Monitor > > DiscoverMonitorsAsync ( CancellationToken cancellationToken = default )
{
2025-12-10 08:43:44 +08:00
try
{
// Get monitor display info from QueryDisplayConfig, keyed by device path (unique per target)
var allMonitorDisplayInfo = DdcCiNative . GetAllMonitorDisplayInfo ( ) ;
2025-12-10 06:21:50 +08:00
2025-12-10 08:43:44 +08:00
// Phase 1: Collect candidate monitors
var monitorHandles = EnumerateMonitorHandles ( ) ;
if ( monitorHandles . Count = = 0 )
{
return Enumerable . Empty < Monitor > ( ) ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 08:43:44 +08:00
var candidateMonitors = await CollectCandidateMonitorsAsync (
monitorHandles , allMonitorDisplayInfo , cancellationToken ) ;
2025-10-20 16:22:47 +08:00
2025-12-10 08:43:44 +08:00
if ( candidateMonitors . Count = = 0 )
{
return Enumerable . Empty < Monitor > ( ) ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 08:43:44 +08:00
// Phase 2: Fetch capabilities in parallel
var fetchResults = await FetchCapabilitiesInParallelAsync (
candidateMonitors , cancellationToken ) ;
2025-10-20 16:22:47 +08:00
2025-12-10 08:43:44 +08:00
// Phase 3: Create monitor objects
return CreateValidMonitors ( fetchResults ) ;
}
catch ( Exception ex )
{
Logger . LogError ( $"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}" ) ;
return Enumerable . Empty < Monitor > ( ) ;
}
2025-12-10 06:21:50 +08:00
}
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
/// <summary>
/// Enumerate all logical monitor handles using Win32 API.
/// </summary>
private List < IntPtr > EnumerateMonitorHandles ( )
{
var handles = new List < IntPtr > ( ) ;
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
bool EnumProc ( IntPtr hMonitor , IntPtr hdcMonitor , IntPtr lprcMonitor , IntPtr dwData )
{
handles . Add ( hMonitor ) ;
return true ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
if ( ! EnumDisplayMonitors ( IntPtr . Zero , IntPtr . Zero , EnumProc , IntPtr . Zero ) )
{
Logger . LogWarning ( "DDC: EnumDisplayMonitors failed" ) ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
return handles ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 08:40:44 +08:00
/// <summary>
/// Get GDI device name for a monitor handle (e.g., "\\.\DISPLAY1").
/// </summary>
private unsafe string? GetGdiDeviceName ( IntPtr hMonitor )
{
var monitorInfo = new MONITORINFOEX { CbSize = ( uint ) sizeof ( MONITORINFOEX ) } ;
if ( GetMonitorInfo ( hMonitor , ref monitorInfo ) )
{
return monitorInfo . GetDeviceName ( ) ;
}
return null ;
}
2025-12-10 06:21:50 +08:00
/// <summary>
/// Phase 1: Collect all candidate monitors with their physical handles.
2025-12-10 08:40:44 +08:00
/// Matches physical monitors with MonitorDisplayInfo using GDI device name and friendly name.
/// Supports mirror mode where multiple physical monitors share the same GDI name.
2025-12-10 06:21:50 +08:00
/// </summary>
private async Task < List < CandidateMonitor > > CollectCandidateMonitorsAsync (
List < IntPtr > monitorHandles ,
2025-12-10 08:40:44 +08:00
Dictionary < string , MonitorDisplayInfo > allMonitorDisplayInfo ,
2025-12-10 06:21:50 +08:00
CancellationToken cancellationToken )
{
var candidates = new List < CandidateMonitor > ( ) ;
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
foreach ( var hMonitor in monitorHandles )
{
2025-12-10 08:40:44 +08:00
// Get GDI device name for this monitor (e.g., "\\.\DISPLAY1")
var gdiDeviceName = GetGdiDeviceName ( hMonitor ) ;
if ( string . IsNullOrEmpty ( gdiDeviceName ) )
{
Logger . LogWarning ( $"DDC: Failed to get GDI device name for hMonitor 0x{hMonitor:X}" ) ;
continue ;
}
2025-12-10 06:21:50 +08:00
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync ( hMonitor , cancellationToken ) ;
if ( physicalMonitors = = null | | physicalMonitors . Length = = 0 )
{
2025-12-10 08:40:44 +08:00
Logger . LogWarning ( $"DDC: Failed to get physical monitors for {gdiDeviceName} after retries" ) ;
2025-12-10 06:21:50 +08:00
continue ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 08:40:44 +08:00
// Find all MonitorDisplayInfo entries that match this GDI device name
// In mirror mode, multiple targets share the same GDI name
var matchingInfos = allMonitorDisplayInfo . Values
. Where ( info = > string . Equals ( info . GdiDeviceName , gdiDeviceName , StringComparison . OrdinalIgnoreCase ) )
. ToList ( ) ;
if ( matchingInfos . Count = = 0 )
2025-12-10 06:47:39 +08:00
{
2025-12-10 08:40:44 +08:00
Logger . LogWarning ( $"DDC: No QueryDisplayConfig info for {gdiDeviceName}, skipping" ) ;
continue ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 08:40:44 +08:00
for ( int i = 0 ; i < physicalMonitors . Length ; i + + )
{
var physicalMonitor = physicalMonitors [ i ] ;
if ( i > = matchingInfos . Count )
{
Logger . LogWarning ( $"DDC: Physical monitor index {i} exceeds available QueryDisplayConfig entries ({matchingInfos.Count}) for {gdiDeviceName}" ) ;
break ;
}
var monitorInfo = matchingInfos [ i ] ;
// Generate stable device key using DevicePath hash for uniqueness
2025-12-10 06:47:39 +08:00
var deviceKey = ! string . IsNullOrEmpty ( monitorInfo . HardwareId )
? $"{monitorInfo.HardwareId}_{monitorInfo.MonitorNumber}"
: $"Unknown_{monitorInfo.MonitorNumber}" ;
2025-12-10 06:21:50 +08:00
2025-12-10 06:47:39 +08:00
var ( handleToUse , _ ) = _handleManager . ReuseOrCreateHandle ( deviceKey , physicalMonitor . HPhysicalMonitor ) ;
2025-10-20 16:22:47 +08:00
2025-12-10 06:47:39 +08:00
var monitorToCreate = physicalMonitor ;
monitorToCreate . HPhysicalMonitor = handleToUse ;
candidates . Add ( new CandidateMonitor ( handleToUse , monitorToCreate , monitorInfo ) ) ;
2025-12-10 08:40:44 +08:00
Logger . LogDebug ( $"DDC: Candidate {gdiDeviceName} -> DevicePath={monitorInfo.DevicePath}, HardwareId={monitorInfo.HardwareId}" ) ;
2025-12-10 06:47:39 +08:00
}
2025-12-10 06:21:50 +08:00
}
2025-12-10 06:47:39 +08:00
return candidates ;
2025-12-10 06:21:50 +08:00
}
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
/// <summary>
/// Phase 2: Fetch DDC/CI capabilities in parallel for all candidate monitors.
/// This is the slow I2C operation (~4s per monitor), but parallelization
/// significantly reduces total time when multiple monitors are connected.
/// </summary>
private async Task < ( CandidateMonitor Candidate , DdcCiValidationResult Result ) [ ] > FetchCapabilitiesInParallelAsync (
List < CandidateMonitor > candidates ,
CancellationToken cancellationToken )
{
Logger . LogInfo ( $"DDC: Phase 2 - Fetching capabilities for {candidates.Count} monitors in parallel" ) ;
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
var tasks = candidates . Select ( candidate = >
Task . Run (
( ) = > ( Candidate : candidate , Result : DdcCiNative . FetchCapabilities ( candidate . Handle ) ) ,
cancellationToken ) ) ;
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
var results = await Task . WhenAll ( tasks ) ;
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
Logger . LogInfo ( $"DDC: Phase 2 completed - Got results for {results.Length} monitors" ) ;
return results ;
}
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
/// <summary>
/// Phase 3: Create Monitor objects for valid DDC/CI monitors.
/// A monitor is valid if it has capabilities with brightness support.
/// </summary>
private List < Monitor > CreateValidMonitors (
2025-12-10 06:47:39 +08:00
( CandidateMonitor Candidate , DdcCiValidationResult Result ) [ ] fetchResults )
2025-12-10 06:21:50 +08:00
{
var monitors = new List < Monitor > ( ) ;
var newHandleMap = new Dictionary < string , IntPtr > ( ) ;
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
foreach ( var ( candidate , capResult ) in fetchResults )
{
if ( ! capResult . IsValid )
{
Logger . LogDebug ( $"DDC: Handle 0x{candidate.Handle:X} - No DDC/CI brightness support, skipping" ) ;
continue ;
}
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
var monitor = _discoveryHelper . CreateMonitorFromPhysical (
candidate . PhysicalMonitor ,
2025-12-10 06:47:39 +08:00
candidate . MonitorInfo ) ;
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
if ( monitor = = null )
{
continue ;
}
2025-11-27 17:34:44 +08:00
2025-12-10 11:00:36 +08:00
// Set capabilities data
2025-12-10 08:40:44 +08:00
if ( ! string . IsNullOrEmpty ( capResult . CapabilitiesString ) )
{
monitor . CapabilitiesRaw = capResult . CapabilitiesString ;
}
if ( capResult . VcpCapabilitiesInfo ! = null )
{
monitor . VcpCapabilitiesInfo = capResult . VcpCapabilitiesInfo ;
2025-12-10 11:00:36 +08:00
UpdateMonitorCapabilitiesFromVcp ( monitor , capResult . VcpCapabilitiesInfo ) ;
// Initialize input source if supported
if ( monitor . SupportsInputSource )
{
InitializeInputSource ( monitor , candidate . Handle ) ;
}
2025-12-10 11:18:28 +08:00
// Initialize color temperature if supported
if ( monitor . SupportsColorTemperature )
{
InitializeColorTemperature ( monitor , candidate . Handle ) ;
}
2025-12-10 08:40:44 +08:00
}
2025-10-20 16:22:47 +08:00
2025-12-10 06:21:50 +08:00
monitors . Add ( monitor ) ;
newHandleMap [ monitor . DeviceKey ] = candidate . Handle ;
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
Logger . LogInfo ( $"DDC: Added monitor {monitor.Id} with {monitor.VcpCapabilitiesInfo?.SupportedVcpCodes.Count ?? 0} VCP codes" ) ;
}
2025-11-27 17:34:44 +08:00
2025-12-10 06:21:50 +08:00
_handleManager . UpdateHandleMap ( newHandleMap ) ;
return monitors ;
}
2025-10-20 16:22:47 +08:00
2025-12-10 11:00:36 +08:00
/// <summary>
/// Initialize input source value for a monitor using VCP 0x60.
/// </summary>
private static void InitializeInputSource ( Monitor monitor , IntPtr handle )
{
if ( DdcCiNative . TryGetVCPFeature ( handle , VcpCodeInputSource , out uint current , out uint _ ) )
{
monitor . CurrentInputSource = ( int ) current ;
Logger . LogDebug ( $"[{monitor.Id}] Input source: {VcpValueNames.GetFormattedName(VcpCodeInputSource, (int)current)}" ) ;
}
}
2025-12-10 11:18:28 +08:00
/// <summary>
/// Initialize color temperature value for a monitor using VCP 0x14.
/// </summary>
private static void InitializeColorTemperature ( Monitor monitor , IntPtr handle )
{
if ( DdcCiNative . TryGetVCPFeature ( handle , VcpCodeSelectColorPreset , out uint current , out uint _ ) )
{
monitor . CurrentColorTemperature = ( int ) current ;
Logger . LogDebug ( $"[{monitor.Id}] Color temperature: {VcpValueNames.GetFormattedName(VcpCodeSelectColorPreset, (int)current)}" ) ;
}
}
2025-12-10 11:00:36 +08:00
/// <summary>
/// Update monitor capability flags based on parsed VCP capabilities.
/// </summary>
private static void UpdateMonitorCapabilitiesFromVcp ( Monitor monitor , VcpCapabilities vcpCaps )
{
// Check for Contrast support (VCP 0x12)
if ( vcpCaps . SupportsVcpCode ( VcpCodeContrast ) )
{
monitor . Capabilities | = MonitorCapabilities . Contrast ;
}
// Check for Volume support (VCP 0x62)
if ( vcpCaps . SupportsVcpCode ( VcpCodeVolume ) )
{
monitor . Capabilities | = MonitorCapabilities . Volume ;
}
// Check for Color Temperature support (VCP 0x14)
if ( vcpCaps . SupportsVcpCode ( VcpCodeSelectColorPreset ) )
{
monitor . SupportsColorTemperature = true ;
}
Logger . LogDebug ( $"[{monitor.Id}] Capabilities: Contrast={monitor.SupportsContrast}, Volume={monitor.SupportsVolume}, ColorTemp={monitor.SupportsColorTemperature}, InputSource={monitor.SupportsInputSource}" ) ;
}
2025-11-18 01:42:10 +08:00
/// <summary>
2025-12-10 06:21:50 +08:00
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles.
/// NULL handles are automatically filtered out by GetPhysicalMonitors; retry if any were filtered.
2025-11-18 01:42:10 +08:00
/// </summary>
/// <param name="hMonitor">Handle to the monitor</param>
/// <param name="cancellationToken">Cancellation token</param>
2025-12-10 06:21:50 +08:00
/// <returns>Array of valid physical monitors, or null if failed after retries</returns>
2025-11-18 01:42:10 +08:00
private async Task < PHYSICAL_MONITOR [ ] ? > GetPhysicalMonitorsWithRetryAsync (
IntPtr hMonitor ,
CancellationToken cancellationToken )
{
const int maxRetries = 3 ;
const int retryDelayMs = 200 ;
for ( int attempt = 0 ; attempt < maxRetries ; attempt + + )
{
if ( attempt > 0 )
{
await Task . Delay ( retryDelayMs , cancellationToken ) ;
}
2025-12-10 06:21:50 +08:00
var monitors = _discoveryHelper . GetPhysicalMonitors ( hMonitor , out bool hasNullHandles ) ;
2025-11-18 01:42:10 +08:00
2025-12-10 06:21:50 +08:00
// Success: got valid monitors with no NULL handles filtered out
if ( monitors ! = null & & ! hasNullHandles )
2025-11-18 01:42:10 +08:00
{
return monitors ;
}
2025-12-10 06:21:50 +08:00
// Got monitors but some had NULL handles - retry to see if API stabilizes
if ( monitors ! = null & & hasNullHandles & & attempt < maxRetries - 1 )
2025-11-18 01:42:10 +08:00
{
2025-12-10 06:21:50 +08:00
Logger . LogWarning ( $"DDC: Some monitors had NULL handles on attempt {attempt + 1}, will retry" ) ;
2025-11-18 01:42:10 +08:00
continue ;
}
2025-12-10 06:21:50 +08:00
// No monitors returned - retry
if ( monitors = = null & & attempt < maxRetries - 1 )
2025-11-18 01:42:10 +08:00
{
2025-12-10 06:21:50 +08:00
Logger . LogWarning ( $"DDC: GetPhysicalMonitors returned null on attempt {attempt + 1}, will retry" ) ;
continue ;
2025-11-18 01:42:10 +08:00
}
2025-12-10 06:21:50 +08:00
// Last attempt - return whatever we have (may have NULL handles filtered)
if ( monitors ! = null & & hasNullHandles )
2025-11-18 01:42:10 +08:00
{
2025-12-10 06:21:50 +08:00
Logger . LogWarning ( $"DDC: NULL handles still present after {maxRetries} attempts, using filtered result" ) ;
2025-11-18 01:42:10 +08:00
}
2025-12-10 06:21:50 +08:00
return monitors ;
2025-11-18 01:42:10 +08:00
}
2025-12-10 06:21:50 +08:00
return null ;
2025-11-18 01:42:10 +08:00
}
2025-10-20 16:22:47 +08:00
/// <summary>
2025-12-10 06:21:50 +08:00
/// Generic method to get VCP feature value with optional logging.
2025-10-20 16:22:47 +08:00
/// </summary>
2025-12-10 06:21:50 +08:00
/// <param name="monitor">Monitor to query</param>
/// <param name="vcpCode">VCP code to read</param>
/// <param name="featureName">Optional feature name for logging (e.g., "color temperature", "input source")</param>
/// <param name="cancellationToken">Cancellation token</param>
2025-10-20 16:22:47 +08:00
private async Task < BrightnessInfo > GetVcpFeatureAsync (
Monitor monitor ,
byte vcpCode ,
2025-12-10 06:21:50 +08:00
string? featureName = null ,
2025-10-20 16:22:47 +08:00
CancellationToken cancellationToken = default )
{
return await Task . Run (
( ) = >
{
if ( monitor . Handle = = IntPtr . Zero )
{
2025-12-10 06:21:50 +08:00
if ( featureName ! = null )
{
Logger . LogDebug ( $"[{monitor.Id}] Invalid handle for {featureName} read" ) ;
}
2025-10-20 16:22:47 +08:00
return BrightnessInfo . Invalid ;
}
if ( DdcCiNative . TryGetVCPFeature ( monitor . Handle , vcpCode , out uint current , out uint max ) )
{
2025-12-10 06:21:50 +08:00
if ( featureName ! = null )
{
var valueName = VcpValueNames . GetFormattedName ( vcpCode , ( int ) current ) ;
Logger . LogDebug ( $"[{monitor.Id}] {featureName} via 0x{vcpCode:X2}: {valueName}" ) ;
}
2025-10-20 16:22:47 +08:00
return new BrightnessInfo ( ( int ) current , 0 , ( int ) max ) ;
}
2025-12-10 06:21:50 +08:00
if ( featureName ! = null )
{
Logger . LogWarning ( $"[{monitor.Id}] Failed to read {featureName} (0x{vcpCode:X2} not supported)" ) ;
}
2025-10-20 16:22:47 +08:00
return BrightnessInfo . Invalid ;
} ,
cancellationToken ) ;
}
2025-12-10 06:21:50 +08:00
/// <summary>
/// Validate that a discrete VCP value is supported by the monitor.
/// Returns null if valid, or a failure result if invalid.
/// </summary>
/// <param name="monitor">Monitor to validate against</param>
/// <param name="vcpCode">VCP code to check</param>
/// <param name="value">Value to validate</param>
/// <param name="featureName">Feature name for error messages</param>
/// <returns>Null if valid, MonitorOperationResult.Failure if invalid</returns>
private static MonitorOperationResult ? ValidateDiscreteVcpValue (
Monitor monitor ,
byte vcpCode ,
int value ,
string featureName )
{
var capabilities = monitor . VcpCapabilitiesInfo ;
if ( capabilities = = null | | ! capabilities . SupportsVcpCode ( vcpCode ) )
{
return null ; // No capabilities to validate against, allow the operation
}
var supportedValues = capabilities . GetSupportedValues ( vcpCode ) ;
if ( supportedValues = = null | | supportedValues . Count = = 0 | | supportedValues . Contains ( value ) )
{
return null ; // Value is valid or no discrete values defined
}
var supportedList = string . Join ( ", " , supportedValues . Select ( v = > $"0x{v:X2}" ) ) ;
Logger . LogWarning ( $"[{monitor.Id}] {featureName} 0x{value:X2} not in supported list: [{supportedList}]" ) ;
return MonitorOperationResult . Failure ( $"{featureName} 0x{value:X2} not supported by monitor" ) ;
}
2025-10-20 16:22:47 +08:00
/// <summary>
/// Generic method to set VCP feature value
/// </summary>
private async Task < MonitorOperationResult > SetVcpFeatureAsync (
Monitor monitor ,
byte vcpCode ,
int value ,
int min = 0 ,
int max = 100 ,
CancellationToken cancellationToken = default )
{
value = Math . Clamp ( value , min , max ) ;
return await Task . Run (
2025-11-18 01:42:10 +08:00
async ( ) = >
2025-10-20 16:22:47 +08:00
{
if ( monitor . Handle = = IntPtr . Zero )
{
return MonitorOperationResult . Failure ( "Invalid monitor handle" ) ;
}
try
{
// Get current value to determine range
2025-11-18 01:42:10 +08:00
var currentInfo = await GetVcpFeatureAsync ( monitor , vcpCode ) ;
2025-10-20 16:22:47 +08:00
if ( ! currentInfo . IsValid )
{
return MonitorOperationResult . Failure ( $"Cannot read current value for VCP 0x{vcpCode:X2}" ) ;
}
uint targetValue = ( uint ) currentInfo . FromPercentage ( value ) ;
if ( DdcCiNative . TrySetVCPFeature ( monitor . Handle , vcpCode , targetValue ) )
{
return MonitorOperationResult . Success ( ) ;
}
var lastError = GetLastError ( ) ;
return MonitorOperationResult . Failure ( $"Failed to set VCP 0x{vcpCode:X2}" , ( int ) lastError ) ;
}
catch ( Exception ex )
{
return MonitorOperationResult . Failure ( $"Exception setting VCP 0x{vcpCode:X2}: {ex.Message}" ) ;
}
} ,
cancellationToken ) ;
}
/// <summary>
2025-12-04 05:44:59 +08:00
/// Core implementation for getting brightness information using VCP code 0x10.
2025-10-20 16:22:47 +08:00
/// </summary>
2025-11-26 05:02:49 +08:00
/// <param name="monitorId">Monitor ID for logging.</param>
/// <param name="physicalHandle">Physical monitor handle.</param>
/// <returns>BrightnessInfo with current, min, and max values, or Invalid if failed.</returns>
private BrightnessInfo GetBrightnessInfoCore ( string monitorId , IntPtr physicalHandle )
2025-10-20 16:22:47 +08:00
{
if ( physicalHandle = = IntPtr . Zero )
{
2025-11-26 05:02:49 +08:00
Logger . LogDebug ( $"[{monitorId}] Invalid physical handle" ) ;
2025-10-20 16:22:47 +08:00
return BrightnessInfo . Invalid ;
}
2025-12-04 05:44:59 +08:00
if ( DdcCiNative . TryGetVCPFeature ( physicalHandle , VcpCodeBrightness , out uint current , out uint max ) )
2025-10-20 16:22:47 +08:00
{
2025-11-26 05:02:49 +08:00
Logger . LogDebug ( $"[{monitorId}] Brightness via VCP 0x10: {current}/{max}" ) ;
2025-10-20 16:22:47 +08:00
return new BrightnessInfo ( ( int ) current , 0 , ( int ) max ) ;
}
return BrightnessInfo . Invalid ;
}
/// <summary>
/// Get physical handle for monitor using stable deviceKey
/// </summary>
private IntPtr GetPhysicalHandle ( Monitor monitor )
{
return _handleManager . GetPhysicalHandle ( monitor ) ;
}
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
protected virtual void Dispose ( bool disposing )
{
if ( ! _disposed & & disposing )
{
_handleManager ? . Dispose ( ) ;
_disposed = true ;
}
}
}
}