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 21:58:34 +08:00
|
|
|
using PowerDisplay.Common.Drivers;
|
|
|
|
|
using PowerDisplay.Common.Drivers.DDC;
|
|
|
|
|
using PowerDisplay.Common.Drivers.WMI;
|
2025-11-24 18:08:11 +08:00
|
|
|
using PowerDisplay.Common.Interfaces;
|
|
|
|
|
using PowerDisplay.Common.Models;
|
2025-11-28 05:08:55 +08:00
|
|
|
using PowerDisplay.Common.Services;
|
2025-11-24 18:08:11 +08:00
|
|
|
using PowerDisplay.Common.Utils;
|
|
|
|
|
using Monitor = PowerDisplay.Common.Models.Monitor;
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-12-04 06:14:01 +08:00
|
|
|
namespace PowerDisplay.Helpers
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Monitor manager for unified control of all monitors
|
|
|
|
|
/// No interface abstraction - KISS principle (only one implementation needed)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public partial class MonitorManager : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly List<Monitor> _monitors = new();
|
2025-11-24 23:36:25 +08:00
|
|
|
private readonly Dictionary<string, Monitor> _monitorLookup = new();
|
2025-10-20 16:22:47 +08:00
|
|
|
private readonly List<IMonitorController> _controllers = new();
|
|
|
|
|
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
2025-11-28 05:08:55 +08:00
|
|
|
private readonly DisplayRotationService _rotationService = new();
|
2025-10-20 16:22:47 +08:00
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
|
|
|
|
|
|
|
|
|
|
public MonitorManager()
|
|
|
|
|
{
|
|
|
|
|
// Initialize controllers
|
|
|
|
|
InitializeControllers();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize controllers
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void InitializeControllers()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// DDC/CI controller (external monitors)
|
|
|
|
|
_controllers.Add(new DdcCiController());
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// WMI controller (internal monitors)
|
|
|
|
|
// First check if WMI is available
|
|
|
|
|
if (WmiController.IsWmiAvailable())
|
|
|
|
|
{
|
|
|
|
|
_controllers.Add(new WmiController());
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-11-13 14:14:49 +08:00
|
|
|
Logger.LogWarning("WMI brightness control not available on this system");
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-26 05:02:49 +08:00
|
|
|
/// Discover all monitors from all controllers.
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
|
|
|
|
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
await _discoveryLock.WaitAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var oldMonitors = _monitors.ToList();
|
|
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
// Step 1: Discover monitors from all controllers
|
|
|
|
|
var discoveryResults = await DiscoverFromAllControllersAsync(cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
// Step 2: Initialize and validate all discovered monitors
|
|
|
|
|
var newMonitors = await InitializeAllMonitorsAsync(discoveryResults, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
// Step 3: Update collection and notify changes
|
|
|
|
|
UpdateMonitorCollection(oldMonitors, newMonitors);
|
2025-11-19 15:08:00 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
return _monitors.AsReadOnly();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_discoveryLock.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Discover monitors from all registered controllers in parallel.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<List<(IMonitorController Controller, List<Monitor> Monitors)>> DiscoverFromAllControllersAsync(
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var discoveryTasks = _controllers.Select(async controller =>
|
|
|
|
|
{
|
|
|
|
|
try
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
|
|
|
|
|
return (Controller: controller, Monitors: monitors.ToList());
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
|
|
|
|
|
return (Controller: controller, Monitors: new List<Monitor>());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var results = await Task.WhenAll(discoveryTasks);
|
|
|
|
|
return results.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize all discovered monitors from all controllers.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<List<Monitor>> InitializeAllMonitorsAsync(
|
|
|
|
|
List<(IMonitorController Controller, List<Monitor> Monitors)> discoveryResults,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var newMonitors = new List<Monitor>();
|
|
|
|
|
|
|
|
|
|
foreach (var (controller, monitors) in discoveryResults)
|
|
|
|
|
{
|
|
|
|
|
var initTasks = monitors.Select(monitor =>
|
|
|
|
|
InitializeSingleMonitorAsync(monitor, controller, cancellationToken));
|
|
|
|
|
|
|
|
|
|
var initializedMonitors = await Task.WhenAll(initTasks);
|
|
|
|
|
var validMonitors = initializedMonitors.Where(m => m != null).Cast<Monitor>();
|
|
|
|
|
newMonitors.AddRange(validMonitors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return newMonitors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize a single monitor: verify control, get brightness, and fetch capabilities.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<Monitor?> InitializeSingleMonitorAsync(
|
|
|
|
|
Monitor monitor,
|
|
|
|
|
IMonitorController controller,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
2025-11-27 17:34:44 +08:00
|
|
|
// Skip control verification if monitor was already validated during discovery phase
|
|
|
|
|
// The presence of cached VcpCapabilitiesInfo indicates the monitor passed DDC/CI validation
|
|
|
|
|
// This avoids redundant capabilities retrieval (~4 seconds per monitor)
|
|
|
|
|
bool alreadyValidated = monitor.VcpCapabilitiesInfo != null &&
|
|
|
|
|
monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0;
|
|
|
|
|
|
|
|
|
|
if (!alreadyValidated)
|
2025-11-26 05:02:49 +08:00
|
|
|
{
|
2025-11-27 17:34:44 +08:00
|
|
|
// Verify if monitor can be controlled (for monitors not validated in discovery phase)
|
|
|
|
|
if (!await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current brightness
|
|
|
|
|
await InitializeMonitorBrightnessAsync(monitor, controller, cancellationToken);
|
|
|
|
|
|
|
|
|
|
// Get capabilities for DDC/CI monitors
|
|
|
|
|
if (monitor.CommunicationMethod?.Contains("DDC", StringComparison.OrdinalIgnoreCase) == true)
|
|
|
|
|
{
|
|
|
|
|
await InitializeMonitorCapabilitiesAsync(monitor, controller, cancellationToken);
|
2025-11-27 14:51:31 +08:00
|
|
|
|
|
|
|
|
// Initialize input source if supported
|
|
|
|
|
if (monitor.SupportsInputSource)
|
|
|
|
|
{
|
|
|
|
|
await InitializeMonitorInputSourceAsync(monitor, controller, cancellationToken);
|
|
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
return monitor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize monitor brightness values.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task InitializeMonitorBrightnessAsync(
|
|
|
|
|
Monitor monitor,
|
|
|
|
|
IMonitorController controller,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
|
|
|
|
if (brightnessInfo.IsValid)
|
2025-11-24 23:36:25 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
|
|
|
|
|
monitor.MinBrightness = brightnessInfo.Minimum;
|
|
|
|
|
monitor.MaxBrightness = brightnessInfo.Maximum;
|
2025-11-24 23:36:25 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize monitor DDC/CI capabilities.
|
2025-11-27 17:34:44 +08:00
|
|
|
/// If capabilities are already cached from discovery phase, only update derived properties.
|
2025-11-26 05:02:49 +08:00
|
|
|
/// </summary>
|
|
|
|
|
private async Task InitializeMonitorCapabilitiesAsync(
|
|
|
|
|
Monitor monitor,
|
|
|
|
|
IMonitorController controller,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-11-27 17:34:44 +08:00
|
|
|
// Check if capabilities were already cached during discovery phase
|
|
|
|
|
// This avoids expensive I2C calls (~4 seconds per monitor) for redundant data
|
|
|
|
|
if (monitor.VcpCapabilitiesInfo != null && monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogInfo($"Using cached capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes");
|
|
|
|
|
UpdateMonitorCapabilitiesFromVcp(monitor);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
Logger.LogInfo($"Getting capabilities for monitor {monitor.Id}");
|
|
|
|
|
var capsString = await controller.GetCapabilitiesStringAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
if (!string.IsNullOrEmpty(capsString))
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-26 05:02:49 +08:00
|
|
|
monitor.CapabilitiesRaw = capsString;
|
2025-12-03 01:32:06 +08:00
|
|
|
var parseResult = Common.Utils.MccsCapabilitiesParser.Parse(capsString);
|
|
|
|
|
monitor.VcpCapabilitiesInfo = parseResult.Capabilities;
|
|
|
|
|
|
|
|
|
|
if (parseResult.HasErrors)
|
|
|
|
|
{
|
|
|
|
|
foreach (var error in parseResult.Errors)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug($"Capabilities parse warning at {error.Position}: {error.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
|
|
|
|
|
Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes");
|
|
|
|
|
|
|
|
|
|
if (monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
UpdateMonitorCapabilitiesFromVcp(monitor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Got empty capabilities string for monitor {monitor.Id}");
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to get capabilities for monitor {monitor.Id}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-20 16:22:47 +08:00
|
|
|
|
2025-11-27 14:51:31 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize monitor input source value.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task InitializeMonitorInputSourceAsync(
|
|
|
|
|
Monitor monitor,
|
|
|
|
|
IMonitorController controller,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var inputSourceInfo = await controller.GetInputSourceAsync(monitor, cancellationToken);
|
|
|
|
|
if (inputSourceInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
monitor.CurrentInputSource = inputSourceInfo.Current;
|
|
|
|
|
Logger.LogInfo($"[{monitor.Id}] Input source initialized: {monitor.InputSourceName} (0x{monitor.CurrentInputSource:X2})");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to get input source for monitor {monitor.Id}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Update the monitor collection and trigger change events.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void UpdateMonitorCollection(List<Monitor> oldMonitors, List<Monitor> newMonitors)
|
|
|
|
|
{
|
|
|
|
|
// Update monitor list and lookup dictionary
|
|
|
|
|
_monitors.Clear();
|
|
|
|
|
_monitorLookup.Clear();
|
2025-11-26 05:57:26 +08:00
|
|
|
|
|
|
|
|
// Sort by monitor number
|
|
|
|
|
newMonitors.Sort((a, b) => a.MonitorNumber.CompareTo(b.MonitorNumber));
|
|
|
|
|
|
2025-11-26 05:02:49 +08:00
|
|
|
_monitors.AddRange(newMonitors);
|
|
|
|
|
|
|
|
|
|
foreach (var monitor in newMonitors)
|
|
|
|
|
{
|
|
|
|
|
_monitorLookup[monitor.Id] = monitor;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
2025-11-26 05:02:49 +08:00
|
|
|
|
|
|
|
|
// Trigger change events
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Get brightness of the specified monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:53:43 +08:00
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
if (controller == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
|
|
|
|
|
|
|
|
|
// Update cached brightness value
|
|
|
|
|
if (brightnessInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return brightnessInfo;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// Mark monitor as unavailable
|
|
|
|
|
Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}");
|
|
|
|
|
monitor.IsAvailable = false;
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set brightness of the specified monitor
|
|
|
|
|
/// </summary>
|
2025-11-27 22:43:28 +08:00
|
|
|
public Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
|
|
|
|
|
=> ExecuteMonitorOperationAsync(
|
|
|
|
|
monitorId,
|
|
|
|
|
brightness,
|
|
|
|
|
(ctrl, mon, val, ct) => ctrl.SetBrightnessAsync(mon, val, ct),
|
|
|
|
|
(mon, val) => mon.UpdateStatus(val, true),
|
|
|
|
|
cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set brightness of all monitors
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var tasks = _monitors
|
|
|
|
|
.Where(m => m.IsAvailable)
|
|
|
|
|
.Select(async monitor =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return await Task.WhenAll(tasks);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set contrast of the specified monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
|
|
|
|
|
=> ExecuteMonitorOperationAsync(
|
|
|
|
|
monitorId,
|
|
|
|
|
contrast,
|
|
|
|
|
(ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct),
|
|
|
|
|
(mon, val) => mon.CurrentContrast = val,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set volume of the specified monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
|
|
|
|
|
=> ExecuteMonitorOperationAsync(
|
|
|
|
|
monitorId,
|
|
|
|
|
volume,
|
|
|
|
|
(ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct),
|
|
|
|
|
(mon, val) => mon.CurrentVolume = val,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Get monitor color temperature
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:53:43 +08:00
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
if (controller == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
|
|
|
|
|
}
|
2025-11-24 18:08:11 +08:00
|
|
|
catch (Exception ex) when (ex is not OutOfMemoryException)
|
2025-10-20 16:22:47 +08:00
|
|
|
{
|
2025-11-24 18:08:11 +08:00
|
|
|
Logger.LogDebug($"GetColorTemperatureAsync failed: {ex.Message}");
|
2025-10-20 16:22:47 +08:00
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set monitor color temperature
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
|
|
|
|
|
=> ExecuteMonitorOperationAsync(
|
|
|
|
|
monitorId,
|
|
|
|
|
colorTemperature,
|
|
|
|
|
(ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct),
|
|
|
|
|
(mon, val) => mon.CurrentColorTemperature = val,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
|
2025-11-27 14:51:31 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Get current input source for a monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BrightnessInfo> GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
|
|
|
|
if (controller == null)
|
|
|
|
|
{
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await controller.GetInputSourceAsync(monitor, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) when (ex is not OutOfMemoryException)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug($"GetInputSourceAsync failed: {ex.Message}");
|
|
|
|
|
return BrightnessInfo.Invalid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Set input source for a monitor
|
|
|
|
|
/// </summary>
|
|
|
|
|
public Task<MonitorOperationResult> SetInputSourceAsync(string monitorId, int inputSource, CancellationToken cancellationToken = default)
|
|
|
|
|
=> ExecuteMonitorOperationAsync(
|
|
|
|
|
monitorId,
|
|
|
|
|
inputSource,
|
|
|
|
|
(ctrl, mon, val, ct) => ctrl.SetInputSourceAsync(mon, val, ct),
|
|
|
|
|
(mon, val) => mon.CurrentInputSource = val,
|
|
|
|
|
cancellationToken);
|
|
|
|
|
|
2025-11-28 05:08:55 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Set rotation/orientation for a monitor.
|
|
|
|
|
/// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI).
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="monitorId">Monitor ID</param>
|
|
|
|
|
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
|
|
|
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
|
|
|
/// <returns>Operation result</returns>
|
|
|
|
|
public Task<MonitorOperationResult> SetRotationAsync(string monitorId, int orientation, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"[MonitorManager] SetRotation: Monitor not found: {monitorId}");
|
|
|
|
|
return Task.FromResult(MonitorOperationResult.Failure("Monitor not found"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (monitor.MonitorNumber <= 0)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"[MonitorManager] SetRotation: Invalid monitor number for {monitorId}");
|
|
|
|
|
return Task.FromResult(MonitorOperationResult.Failure("Invalid monitor number"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rotation uses Windows display settings API, not DDC/CI controller
|
|
|
|
|
var result = _rotationService.SetRotation(monitor.MonitorNumber, orientation);
|
|
|
|
|
|
|
|
|
|
if (result.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
monitor.Orientation = orientation;
|
|
|
|
|
monitor.LastUpdate = DateTime.Now;
|
|
|
|
|
Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"[MonitorManager] SetRotation: Failed for {monitorId}: {result.ErrorMessage}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.FromResult(result);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 14:51:31 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize input source for a monitor (async operation)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task InitializeInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var sourceInfo = await GetInputSourceAsync(monitorId, cancellationToken);
|
|
|
|
|
if (sourceInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor != null)
|
|
|
|
|
{
|
|
|
|
|
// Store raw VCP 0x60 value (e.g., 0x11 for HDMI-1)
|
|
|
|
|
monitor.CurrentInputSource = sourceInfo.Current;
|
|
|
|
|
Logger.LogInfo($"[{monitorId}] Input source initialized: {monitor.InputSourceName}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to initialize input source for {monitorId}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initialize color temperature for a monitor (async operation)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
|
|
|
|
|
if (tempInfo.IsValid)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor != null)
|
|
|
|
|
{
|
2025-11-14 13:17:55 +08:00
|
|
|
// Store raw VCP 0x14 preset value (e.g., 0x05 for 6500K)
|
|
|
|
|
// No Kelvin conversion - we use discrete presets
|
|
|
|
|
monitor.CurrentColorTemperature = tempInfo.Current;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-11-24 23:36:25 +08:00
|
|
|
/// Get monitor by ID. Uses dictionary lookup for O(1) performance.
|
2025-10-20 16:22:47 +08:00
|
|
|
/// </summary>
|
|
|
|
|
public Monitor? GetMonitor(string monitorId)
|
|
|
|
|
{
|
2025-11-24 23:36:25 +08:00
|
|
|
return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Get controller for the monitor
|
|
|
|
|
/// </summary>
|
2025-11-17 14:53:43 +08:00
|
|
|
private async Task<IMonitorController?> GetControllerForMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
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
|
|
|
// WMI monitors use WmiController, DDC/CI monitors use DdcCiController
|
2025-11-17 14:53:43 +08:00
|
|
|
foreach (var controller in _controllers)
|
|
|
|
|
{
|
|
|
|
|
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
|
|
|
|
{
|
|
|
|
|
return controller;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2025-10-20 16:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generic helper to execute monitor operations with common error handling.
|
|
|
|
|
/// Eliminates code duplication across Set* methods.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task<MonitorOperationResult> ExecuteMonitorOperationAsync<T>(
|
|
|
|
|
string monitorId,
|
|
|
|
|
T value,
|
|
|
|
|
Func<IMonitorController, Monitor, T, CancellationToken, Task<MonitorOperationResult>> operation,
|
|
|
|
|
Action<Monitor, T> onSuccess,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var monitor = GetMonitor(monitorId);
|
|
|
|
|
if (monitor == null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
|
|
|
|
|
return MonitorOperationResult.Failure("Monitor not found");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:53:43 +08:00
|
|
|
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
2025-10-20 16:22:47 +08:00
|
|
|
if (controller == null)
|
|
|
|
|
{
|
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.LogError($"[MonitorManager] No controller available for monitor {monitorId}");
|
2025-10-20 16:22:47 +08:00
|
|
|
return MonitorOperationResult.Failure("No controller available for this monitor");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await operation(controller, monitor, value, cancellationToken);
|
|
|
|
|
|
|
|
|
|
if (result.IsSuccess)
|
|
|
|
|
{
|
|
|
|
|
onSuccess(monitor, value);
|
|
|
|
|
monitor.LastUpdate = DateTime.Now;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
monitor.IsAvailable = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
monitor.IsAvailable = false;
|
|
|
|
|
Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}");
|
|
|
|
|
return MonitorOperationResult.Failure($"Exception: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/// <summary>
|
|
|
|
|
/// Update monitor capability flags based on parsed VCP capabilities
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void UpdateMonitorCapabilitiesFromVcp(Monitor monitor)
|
|
|
|
|
{
|
|
|
|
|
var vcpCaps = monitor.VcpCapabilitiesInfo;
|
|
|
|
|
if (vcpCaps == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Contrast support (VCP 0x12)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeContrast))
|
|
|
|
|
{
|
|
|
|
|
monitor.Capabilities |= MonitorCapabilities.Contrast;
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Contrast support detected via VCP 0x12");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Volume support (VCP 0x62)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeVolume))
|
|
|
|
|
{
|
|
|
|
|
monitor.Capabilities |= MonitorCapabilities.Volume;
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Volume support detected via VCP 0x62");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for Color Temperature support (VCP 0x14)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeSelectColorPreset))
|
|
|
|
|
{
|
|
|
|
|
monitor.SupportsColorTemperature = true;
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Color temperature support detected via VCP 0x14");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 14:51:31 +08:00
|
|
|
// Check for Input Source support (VCP 0x60)
|
|
|
|
|
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeInputSource))
|
|
|
|
|
{
|
|
|
|
|
var supportedSources = vcpCaps.GetSupportedValues(0x60);
|
|
|
|
|
Logger.LogDebug($"[{monitor.Id}] Input source support detected via VCP 0x60: {supportedSources?.Count ?? 0} sources");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.LogInfo($"[{monitor.Id}] Capabilities updated: Contrast={monitor.SupportsContrast}, Volume={monitor.SupportsVolume}, ColorTemp={monitor.SupportsColorTemperature}, InputSource={monitor.SupportsInputSource}");
|
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
|
|
|
}
|
|
|
|
|
|
2025-10-20 16:22:47 +08:00
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Dispose(true);
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (!_disposed && disposing)
|
|
|
|
|
{
|
|
|
|
|
_discoveryLock?.Dispose();
|
|
|
|
|
|
|
|
|
|
// Release all controllers
|
|
|
|
|
foreach (var controller in _controllers)
|
|
|
|
|
{
|
|
|
|
|
controller?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_controllers.Clear();
|
|
|
|
|
_monitors.Clear();
|
2025-11-24 23:36:25 +08:00
|
|
|
_monitorLookup.Clear();
|
2025-10-20 16:22:47 +08:00
|
|
|
_disposed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|