mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +01:00
Update
This commit is contained in:
@@ -187,7 +187,7 @@ namespace PowerDisplay.Core.Models
|
||||
public IntPtr Handle { get; set; } = IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey)
|
||||
/// Device key - unique identifier part of device path
|
||||
/// </summary>
|
||||
public string DeviceKey { get; set; } = string.Empty;
|
||||
|
||||
@@ -216,6 +216,16 @@ namespace PowerDisplay.Core.Models
|
||||
/// </summary>
|
||||
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
|
||||
|
||||
/// <summary>
|
||||
/// Raw DDC/CI capabilities string (MCCS format)
|
||||
/// </summary>
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parsed VCP capabilities information
|
||||
/// </summary>
|
||||
public VcpCapabilities? VcpCapabilitiesInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last update time
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// DDC/CI VCP capabilities information
|
||||
/// </summary>
|
||||
public class VcpCapabilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw capabilities string (MCCS format)
|
||||
/// </summary>
|
||||
public string Raw { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor model name from capabilities
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Monitor type from capabilities (e.g., "LCD")
|
||||
/// </summary>
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MCCS protocol version
|
||||
/// </summary>
|
||||
public string? Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported command codes
|
||||
/// </summary>
|
||||
public List<byte> SupportedCommands { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Supported VCP codes with their information
|
||||
/// </summary>
|
||||
public Dictionary<byte, VcpCodeInfo> SupportedVcpCodes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific VCP code is supported
|
||||
/// </summary>
|
||||
public bool SupportsVcpCode(byte code) => SupportedVcpCodes.ContainsKey(code);
|
||||
|
||||
/// <summary>
|
||||
/// Get VCP code information
|
||||
/// </summary>
|
||||
public VcpCodeInfo? GetVcpCodeInfo(byte code)
|
||||
{
|
||||
return SupportedVcpCodes.TryGetValue(code, out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a VCP code supports discrete values
|
||||
/// </summary>
|
||||
public bool HasDiscreteValues(byte code)
|
||||
{
|
||||
var info = GetVcpCodeInfo(code);
|
||||
return info?.HasDiscreteValues ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get supported values for a VCP code
|
||||
/// </summary>
|
||||
public IReadOnlyList<int>? GetSupportedValues(byte code)
|
||||
{
|
||||
return GetVcpCodeInfo(code)?.SupportedValues;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty capabilities object
|
||||
/// </summary>
|
||||
public static VcpCapabilities Empty => new();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Model: {Model}, VCP Codes: {SupportedVcpCodes.Count}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a single VCP code
|
||||
/// </summary>
|
||||
public readonly struct VcpCodeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code (e.g., 0x10 for brightness)
|
||||
/// </summary>
|
||||
public byte Code { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable name of the VCP code
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported discrete values (empty if continuous range)
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> SupportedValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this VCP code has discrete values
|
||||
/// </summary>
|
||||
public bool HasDiscreteValues => SupportedValues.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this VCP code supports a continuous range
|
||||
/// </summary>
|
||||
public bool IsContinuous => SupportedValues.Count == 0;
|
||||
|
||||
public VcpCodeInfo(byte code, string name, IReadOnlyList<int>? supportedValues = null)
|
||||
{
|
||||
Code = code;
|
||||
Name = name;
|
||||
SupportedValues = supportedValues ?? Array.Empty<int>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (HasDiscreteValues)
|
||||
{
|
||||
return $"0x{Code:X2} ({Name}): {string.Join(", ", SupportedValues)}";
|
||||
}
|
||||
|
||||
return $"0x{Code:X2} ({Name}): Continuous";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,36 @@ namespace PowerDisplay.Core
|
||||
Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Get capabilities for DDC/CI monitors (External type)
|
||||
if (monitor.Type == MonitorType.External && controller.SupportedType == MonitorType.External)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Getting capabilities for monitor {monitor.Id}");
|
||||
var capsString = await controller.GetCapabilitiesStringAsync(monitor, cancellationToken);
|
||||
|
||||
if (!string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
monitor.CapabilitiesRaw = capsString;
|
||||
|
||||
// Parse capabilities
|
||||
monitor.VcpCapabilitiesInfo = Utils.VcpCapabilitiesParser.Parse(capsString);
|
||||
|
||||
Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Got empty capabilities string for monitor {monitor.Id}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to get capabilities for monitor {monitor.Id}: {ex.Message}");
|
||||
|
||||
// Continue without capabilities - not critical
|
||||
}
|
||||
}
|
||||
|
||||
newMonitors.Add(monitor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Models;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Parser for DDC/CI MCCS capabilities strings
|
||||
/// </summary>
|
||||
public static class VcpCapabilitiesParser
|
||||
{
|
||||
private static readonly char[] SpaceSeparator = new[] { ' ' };
|
||||
private static readonly char[] ValueSeparators = new[] { ' ', '(', ')' };
|
||||
|
||||
/// <summary>
|
||||
/// Parse a capabilities string into structured VcpCapabilities
|
||||
/// </summary>
|
||||
/// <param name="capabilitiesString">Raw MCCS capabilities string</param>
|
||||
/// <returns>Parsed capabilities object, or Empty if parsing fails</returns>
|
||||
public static VcpCapabilities Parse(string? capabilitiesString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(capabilitiesString))
|
||||
{
|
||||
return VcpCapabilities.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var capabilities = new VcpCapabilities
|
||||
{
|
||||
Raw = capabilitiesString,
|
||||
};
|
||||
|
||||
// Extract model, type, protocol
|
||||
capabilities.Model = ExtractValue(capabilitiesString, "model");
|
||||
capabilities.Type = ExtractValue(capabilitiesString, "type");
|
||||
capabilities.Protocol = ExtractValue(capabilitiesString, "prot");
|
||||
|
||||
// Extract supported commands
|
||||
capabilities.SupportedCommands = ParseCommandList(capabilitiesString);
|
||||
|
||||
// Extract and parse VCP codes
|
||||
capabilities.SupportedVcpCodes = ParseVcpCodes(capabilitiesString);
|
||||
|
||||
Logger.LogInfo($"Parsed capabilities: Model={capabilities.Model}, VCP Codes={capabilities.SupportedVcpCodes.Count}");
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to parse capabilities string: {ex.Message}");
|
||||
return VcpCapabilities.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract a simple value from capabilities string
|
||||
/// Example: "model(PD3220U)" -> "PD3220U"
|
||||
/// </summary>
|
||||
private static string? ExtractValue(string capabilities, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pattern = $@"{key}\(([^)]+)\)";
|
||||
var match = Regex.Match(capabilities, pattern, RegexOptions.IgnoreCase);
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse command list from capabilities string
|
||||
/// Example: "cmds(01 02 03 07 0C)" -> [0x01, 0x02, 0x03, 0x07, 0x0C]
|
||||
/// </summary>
|
||||
private static List<byte> ParseCommandList(string capabilities)
|
||||
{
|
||||
var commands = new List<byte>();
|
||||
|
||||
try
|
||||
{
|
||||
var match = Regex.Match(capabilities, @"cmds\(([^)]+)\)", RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
var cmdString = match.Groups[1].Value;
|
||||
var cmdTokens = cmdString.Split(SpaceSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var token in cmdTokens)
|
||||
{
|
||||
if (byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cmd))
|
||||
{
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to parse command list: {ex.Message}");
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP codes section from capabilities string
|
||||
/// </summary>
|
||||
private static Dictionary<byte, VcpCodeInfo> ParseVcpCodes(string capabilities)
|
||||
{
|
||||
var vcpCodes = new Dictionary<byte, VcpCodeInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
// Find the "vcp(" section
|
||||
var vcpStart = capabilities.IndexOf("vcp(", StringComparison.OrdinalIgnoreCase);
|
||||
if (vcpStart < 0)
|
||||
{
|
||||
Logger.LogWarning("No 'vcp(' section found in capabilities string");
|
||||
return vcpCodes;
|
||||
}
|
||||
|
||||
// Extract the complete VCP section by matching parentheses
|
||||
var vcpSection = ExtractVcpSection(capabilities, vcpStart + 4); // Skip "vcp("
|
||||
if (string.IsNullOrEmpty(vcpSection))
|
||||
{
|
||||
return vcpCodes;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Extracted VCP section: {vcpSection.Substring(0, Math.Min(100, vcpSection.Length))}...");
|
||||
|
||||
// Parse VCP codes from the section
|
||||
ParseVcpCodesFromSection(vcpSection, vcpCodes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to parse VCP codes: {ex.Message}");
|
||||
}
|
||||
|
||||
return vcpCodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract VCP section by matching parentheses
|
||||
/// </summary>
|
||||
private static string ExtractVcpSection(string capabilities, int startIndex)
|
||||
{
|
||||
var depth = 1;
|
||||
var result = string.Empty;
|
||||
|
||||
for (int i = startIndex; i < capabilities.Length && depth > 0; i++)
|
||||
{
|
||||
var ch = capabilities[i];
|
||||
|
||||
if (ch == '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (ch == ')')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result += ch;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP codes from the extracted VCP section
|
||||
/// </summary>
|
||||
private static void ParseVcpCodesFromSection(string vcpSection, Dictionary<byte, VcpCodeInfo> vcpCodes)
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
while (i < vcpSection.Length)
|
||||
{
|
||||
// Skip whitespace
|
||||
while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i]))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= vcpSection.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Read VCP code (2 hex digits)
|
||||
if (i + 1 < vcpSection.Length &&
|
||||
IsHexDigit(vcpSection[i]) &&
|
||||
IsHexDigit(vcpSection[i + 1]))
|
||||
{
|
||||
var codeStr = vcpSection.Substring(i, 2);
|
||||
if (byte.TryParse(codeStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var code))
|
||||
{
|
||||
i += 2;
|
||||
|
||||
// Check if there are supported values (followed by '(')
|
||||
while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i]))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
var supportedValues = new List<int>();
|
||||
|
||||
if (i < vcpSection.Length && vcpSection[i] == '(')
|
||||
{
|
||||
// Extract supported values
|
||||
i++; // Skip '('
|
||||
var valuesSection = ExtractVcpValuesSection(vcpSection, i);
|
||||
i += valuesSection.Length + 1; // +1 for closing ')'
|
||||
|
||||
// Parse values
|
||||
ParseVcpValues(valuesSection, supportedValues);
|
||||
}
|
||||
|
||||
// Get VCP code name
|
||||
var name = VcpCodeNames.GetName(code);
|
||||
|
||||
// Store VCP code info
|
||||
vcpCodes[code] = new VcpCodeInfo(code, name, supportedValues);
|
||||
|
||||
Logger.LogDebug($"Parsed VCP code: 0x{code:X2} ({name}), Values: {supportedValues.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract VCP values section by matching parentheses
|
||||
/// </summary>
|
||||
private static string ExtractVcpValuesSection(string section, int startIndex)
|
||||
{
|
||||
var depth = 1;
|
||||
var result = string.Empty;
|
||||
|
||||
for (int i = startIndex; i < section.Length && depth > 0; i++)
|
||||
{
|
||||
var ch = section[i];
|
||||
|
||||
if (ch == '(')
|
||||
{
|
||||
depth++;
|
||||
result += ch;
|
||||
}
|
||||
else if (ch == ')')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result += ch;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP values from the values section
|
||||
/// </summary>
|
||||
private static void ParseVcpValues(string valuesSection, List<int> supportedValues)
|
||||
{
|
||||
var tokens = valuesSection.Split(ValueSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
// Try to parse as hex
|
||||
if (int.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
supportedValues.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is a hex digit
|
||||
/// </summary>
|
||||
private static bool IsHexDigit(char c)
|
||||
{
|
||||
return (c >= '0' && c <= '9') ||
|
||||
(c >= 'A' && c <= 'F') ||
|
||||
(c >= 'a' && c <= 'f');
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs
Normal file
239
src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
// 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.Collections.Generic;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code to friendly name mapping based on MCCS v2.2a specification
|
||||
/// </summary>
|
||||
public static class VcpCodeNames
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code to name mapping
|
||||
/// </summary>
|
||||
private static readonly Dictionary<byte, string> CodeNames = new()
|
||||
{
|
||||
// Control codes
|
||||
{ 0x01, "Degauss" },
|
||||
{ 0x02, "New Control Value" },
|
||||
{ 0x03, "Soft Controls" },
|
||||
|
||||
// Geometry codes
|
||||
{ 0x04, "Restore Factory Defaults" },
|
||||
{ 0x05, "Restore Brightness and Contrast" },
|
||||
{ 0x06, "Restore Factory Geometry" },
|
||||
{ 0x08, "Restore Color Defaults" },
|
||||
{ 0x0A, "Restore Factory TV Defaults" },
|
||||
|
||||
// Color temperature codes
|
||||
{ 0x0B, "Color Temperature Increment" },
|
||||
{ 0x0C, "Color Temperature Request" },
|
||||
{ 0x0E, "Clock" },
|
||||
{ 0x0F, "Color Saturation" },
|
||||
|
||||
// Image adjustment codes
|
||||
{ 0x10, "Brightness" },
|
||||
{ 0x11, "Flesh Tone Enhancement" },
|
||||
{ 0x12, "Contrast" },
|
||||
{ 0x13, "Backlight Control" },
|
||||
{ 0x14, "Select Color Preset" },
|
||||
{ 0x16, "Video Gain: Red" },
|
||||
{ 0x17, "User Color Vision Compensation" },
|
||||
{ 0x18, "Video Gain: Green" },
|
||||
{ 0x1A, "Video Gain: Blue" },
|
||||
{ 0x1C, "Focus" },
|
||||
{ 0x1E, "Auto Setup" },
|
||||
{ 0x1F, "Auto Color Setup" },
|
||||
|
||||
// Geometry codes
|
||||
{ 0x20, "Horizontal Position" },
|
||||
{ 0x22, "Horizontal Size" },
|
||||
{ 0x24, "Horizontal Pincushion" },
|
||||
{ 0x26, "Horizontal Pincushion Balance" },
|
||||
{ 0x28, "Horizontal Convergence R/B" },
|
||||
{ 0x29, "Horizontal Convergence M/G" },
|
||||
{ 0x2A, "Horizontal Linearity" },
|
||||
{ 0x2C, "Horizontal Linearity Balance" },
|
||||
{ 0x30, "Vertical Position" },
|
||||
{ 0x32, "Vertical Size" },
|
||||
{ 0x34, "Vertical Pincushion" },
|
||||
{ 0x36, "Vertical Pincushion Balance" },
|
||||
{ 0x38, "Vertical Convergence R/B" },
|
||||
{ 0x39, "Vertical Convergence M/G" },
|
||||
{ 0x3A, "Vertical Linearity" },
|
||||
{ 0x3C, "Vertical Linearity Balance" },
|
||||
{ 0x3E, "Clock Phase" },
|
||||
|
||||
// Miscellaneous codes
|
||||
{ 0x40, "Horizontal Parallelogram" },
|
||||
{ 0x41, "Vertical Parallelogram" },
|
||||
{ 0x42, "Horizontal Keystone" },
|
||||
{ 0x43, "Vertical Keystone" },
|
||||
{ 0x44, "Rotation" },
|
||||
{ 0x46, "Top Corner Flare" },
|
||||
{ 0x48, "Top Corner Hook" },
|
||||
{ 0x4A, "Bottom Corner Flare" },
|
||||
{ 0x4C, "Bottom Corner Hook" },
|
||||
|
||||
// Advanced codes
|
||||
{ 0x52, "Active Control" },
|
||||
{ 0x54, "Performance Preservation" },
|
||||
{ 0x56, "Horizontal Moire" },
|
||||
{ 0x58, "Vertical Moire" },
|
||||
{ 0x59, "6 Axis Saturation: Red" },
|
||||
{ 0x5A, "6 Axis Saturation: Yellow" },
|
||||
{ 0x5B, "6 Axis Saturation: Green" },
|
||||
{ 0x5C, "6 Axis Saturation: Cyan" },
|
||||
{ 0x5D, "6 Axis Saturation: Blue" },
|
||||
{ 0x5E, "6 Axis Saturation: Magenta" },
|
||||
|
||||
// Input source codes
|
||||
{ 0x60, "Input Source" },
|
||||
{ 0x62, "Audio Speaker Volume" },
|
||||
{ 0x63, "Speaker Select" },
|
||||
{ 0x64, "Audio: Microphone Volume" },
|
||||
{ 0x66, "Ambient Light Sensor" },
|
||||
{ 0x6B, "Backlight Level: White" },
|
||||
{ 0x6C, "Video Black Level: Red" },
|
||||
{ 0x6D, "Backlight Level: Red" },
|
||||
{ 0x6E, "Video Black Level: Green" },
|
||||
{ 0x6F, "Backlight Level: Green" },
|
||||
{ 0x70, "Video Black Level: Blue" },
|
||||
{ 0x71, "Backlight Level: Blue" },
|
||||
{ 0x72, "Gamma" },
|
||||
{ 0x73, "LUT Size" },
|
||||
{ 0x74, "Single Point LUT Operation" },
|
||||
{ 0x75, "Block LUT Operation" },
|
||||
|
||||
// Color calibration codes
|
||||
{ 0x86, "Display Scaling" },
|
||||
{ 0x87, "Sharpness" },
|
||||
{ 0x88, "Velocity Scan Modulation" },
|
||||
{ 0x8A, "Color Saturation" },
|
||||
{ 0x8C, "TV Sharpness" },
|
||||
{ 0x8D, "Audio Mute/Screen Blank" },
|
||||
{ 0x8E, "TV Contrast" },
|
||||
{ 0x8F, "Audio Treble" },
|
||||
{ 0x90, "Hue" },
|
||||
{ 0x91, "Audio Bass" },
|
||||
{ 0x92, "TV Black Level/Luminance" },
|
||||
{ 0x93, "Audio Balance L/R" },
|
||||
{ 0x94, "Audio Processor Mode" },
|
||||
{ 0x95, "Window Position(TL_X)" },
|
||||
{ 0x96, "Window Position(TL_Y)" },
|
||||
{ 0x97, "Window Position(BR_X)" },
|
||||
{ 0x98, "Window Position(BR_Y)" },
|
||||
{ 0x99, "Window Background" },
|
||||
{ 0x9A, "6 Axis Hue Control: Red" },
|
||||
{ 0x9B, "6 Axis Hue Control: Yellow" },
|
||||
{ 0x9C, "6 Axis Hue Control: Green" },
|
||||
{ 0x9D, "6 Axis Hue Control: Cyan" },
|
||||
{ 0x9E, "6 Axis Hue Control: Blue" },
|
||||
{ 0x9F, "6 Axis Hue Control: Magenta" },
|
||||
|
||||
// Window control codes
|
||||
{ 0xA0, "Auto Setup On/Off" },
|
||||
{ 0xA2, "Auto Color Setup On/Off" },
|
||||
{ 0xA4, "Window Mask Control" },
|
||||
{ 0xA5, "Window Select" },
|
||||
{ 0xA6, "Window Size" },
|
||||
{ 0xA7, "Window Transparency" },
|
||||
{ 0xAA, "Screen Orientation" },
|
||||
{ 0xAC, "Horizontal Frequency" },
|
||||
{ 0xAE, "Vertical Frequency" },
|
||||
|
||||
// Misc advanced codes
|
||||
{ 0xB0, "Settings" },
|
||||
{ 0xB2, "Flat Panel Sub-Pixel Layout" },
|
||||
{ 0xB4, "Source Timing Mode" },
|
||||
{ 0xB6, "Display Technology Type" },
|
||||
{ 0xB7, "Monitor Status" },
|
||||
{ 0xB8, "Packet Count" },
|
||||
{ 0xB9, "Monitor X Origin" },
|
||||
{ 0xBA, "Monitor Y Origin" },
|
||||
{ 0xBB, "Header Error Count" },
|
||||
{ 0xBC, "Body CRC Error Count" },
|
||||
{ 0xBD, "Client ID" },
|
||||
{ 0xBE, "Link Control" },
|
||||
|
||||
// Display controller codes
|
||||
{ 0xC0, "Display Usage Time" },
|
||||
{ 0xC2, "Display Firmware Level" },
|
||||
{ 0xC4, "Display Descriptor Length" },
|
||||
{ 0xC5, "Transmit Display Descriptor" },
|
||||
{ 0xC6, "Enable Display of 'Display Descriptor'" },
|
||||
{ 0xC8, "Display Controller Type" },
|
||||
{ 0xC9, "Display Firmware Level" },
|
||||
{ 0xCA, "OSD" },
|
||||
{ 0xCC, "OSD Language" },
|
||||
{ 0xD0, "Output Select" },
|
||||
{ 0xD2, "Asset Tag" },
|
||||
{ 0xD4, "Stereo Video Mode" },
|
||||
{ 0xD6, "Power Mode" },
|
||||
{ 0xD7, "Auxiliary Power Output" },
|
||||
{ 0xD8, "Scan Mode" },
|
||||
{ 0xD9, "Image Mode" },
|
||||
{ 0xDA, "On Screen Display" },
|
||||
{ 0xDC, "Display Application" },
|
||||
{ 0xDE, "Scratch Pad" },
|
||||
|
||||
// Information codes
|
||||
{ 0xDF, "VCP Version" },
|
||||
{ 0xE0, "Manufacturer Specific" },
|
||||
{ 0xE1, "Manufacturer Specific" },
|
||||
{ 0xE2, "Manufacturer Specific" },
|
||||
{ 0xE3, "Manufacturer Specific" },
|
||||
{ 0xE4, "Manufacturer Specific" },
|
||||
{ 0xE5, "Manufacturer Specific" },
|
||||
{ 0xE6, "Manufacturer Specific" },
|
||||
{ 0xE7, "Manufacturer Specific" },
|
||||
{ 0xE8, "Manufacturer Specific" },
|
||||
{ 0xE9, "Manufacturer Specific" },
|
||||
{ 0xEA, "Manufacturer Specific" },
|
||||
{ 0xEB, "Manufacturer Specific" },
|
||||
{ 0xEC, "Manufacturer Specific" },
|
||||
{ 0xED, "Manufacturer Specific" },
|
||||
{ 0xEE, "Manufacturer Specific" },
|
||||
{ 0xEF, "Manufacturer Specific" },
|
||||
{ 0xF0, "Manufacturer Specific" },
|
||||
{ 0xF1, "Manufacturer Specific" },
|
||||
{ 0xF2, "Manufacturer Specific" },
|
||||
{ 0xF3, "Manufacturer Specific" },
|
||||
{ 0xF4, "Manufacturer Specific" },
|
||||
{ 0xF5, "Manufacturer Specific" },
|
||||
{ 0xF6, "Manufacturer Specific" },
|
||||
{ 0xF7, "Manufacturer Specific" },
|
||||
{ 0xF8, "Manufacturer Specific" },
|
||||
{ 0xF9, "Manufacturer Specific" },
|
||||
{ 0xFA, "Manufacturer Specific" },
|
||||
{ 0xFB, "Manufacturer Specific" },
|
||||
{ 0xFC, "Manufacturer Specific" },
|
||||
{ 0xFD, "Manufacturer Specific" },
|
||||
{ 0xFE, "Manufacturer Specific" },
|
||||
{ 0xFF, "Manufacturer Specific" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the friendly name for a VCP code
|
||||
/// </summary>
|
||||
/// <param name="code">VCP code (e.g., 0x10)</param>
|
||||
/// <returns>Friendly name, or hex representation if unknown</returns>
|
||||
public static string GetName(byte code)
|
||||
{
|
||||
return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a VCP code has a known name
|
||||
/// </summary>
|
||||
public static bool HasName(byte code) => CodeNames.ContainsKey(code);
|
||||
|
||||
/// <summary>
|
||||
/// Get all known VCP codes
|
||||
/// </summary>
|
||||
public static IEnumerable<byte> GetAllKnownCodes() => CodeNames.Keys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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.Collections.Generic;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides human-readable names for VCP code values based on MCCS standard
|
||||
/// </summary>
|
||||
public static class VcpValueNames
|
||||
{
|
||||
// Dictionary<VcpCode, Dictionary<Value, Name>>
|
||||
private static readonly Dictionary<byte, Dictionary<int, string>> ValueNames = new()
|
||||
{
|
||||
// 0x14: Select Color Preset
|
||||
[0x14] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "sRGB",
|
||||
[0x02] = "Display Native",
|
||||
[0x03] = "4000K",
|
||||
[0x04] = "5000K",
|
||||
[0x05] = "6500K",
|
||||
[0x06] = "7500K",
|
||||
[0x08] = "9300K",
|
||||
[0x09] = "10000K",
|
||||
[0x0A] = "11500K",
|
||||
[0x0B] = "User 1",
|
||||
[0x0C] = "User 2",
|
||||
[0x0D] = "User 3",
|
||||
},
|
||||
|
||||
// 0x60: Input Source
|
||||
[0x60] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "VGA-1",
|
||||
[0x02] = "VGA-2",
|
||||
[0x03] = "DVI-1",
|
||||
[0x04] = "DVI-2",
|
||||
[0x05] = "Composite Video 1",
|
||||
[0x06] = "Composite Video 2",
|
||||
[0x07] = "S-Video-1",
|
||||
[0x08] = "S-Video-2",
|
||||
[0x09] = "Tuner-1",
|
||||
[0x0A] = "Tuner-2",
|
||||
[0x0B] = "Tuner-3",
|
||||
[0x0C] = "Component Video 1",
|
||||
[0x0D] = "Component Video 2",
|
||||
[0x0E] = "Component Video 3",
|
||||
[0x0F] = "DisplayPort-1",
|
||||
[0x10] = "DisplayPort-2",
|
||||
[0x11] = "HDMI-1",
|
||||
[0x12] = "HDMI-2",
|
||||
[0x1B] = "USB-C",
|
||||
},
|
||||
|
||||
// 0xD6: Power Mode
|
||||
[0xD6] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "On",
|
||||
[0x02] = "Standby",
|
||||
[0x03] = "Suspend",
|
||||
[0x04] = "Off (DPM)",
|
||||
[0x05] = "Off (Hard)",
|
||||
},
|
||||
|
||||
// 0x8D: Audio Mute
|
||||
[0x8D] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "Muted",
|
||||
[0x02] = "Unmuted",
|
||||
},
|
||||
|
||||
// 0xDC: Display Application
|
||||
[0xDC] = new Dictionary<int, string>
|
||||
{
|
||||
[0x00] = "Standard/Default",
|
||||
[0x01] = "Productivity",
|
||||
[0x02] = "Mixed",
|
||||
[0x03] = "Movie",
|
||||
[0x04] = "User Defined",
|
||||
[0x05] = "Games",
|
||||
[0x06] = "Sports",
|
||||
[0x07] = "Professional (calibration)",
|
||||
[0x08] = "Standard/Default with intermediate power consumption",
|
||||
[0x09] = "Standard/Default with low power consumption",
|
||||
[0x0A] = "Demonstration",
|
||||
[0xF0] = "Dynamic Contrast",
|
||||
},
|
||||
|
||||
// 0xCC: OSD Language
|
||||
[0xCC] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "Chinese (traditional, Hantai)",
|
||||
[0x02] = "English",
|
||||
[0x03] = "French",
|
||||
[0x04] = "German",
|
||||
[0x05] = "Italian",
|
||||
[0x06] = "Japanese",
|
||||
[0x07] = "Korean",
|
||||
[0x08] = "Portuguese (Portugal)",
|
||||
[0x09] = "Russian",
|
||||
[0x0A] = "Spanish",
|
||||
[0x0B] = "Swedish",
|
||||
[0x0C] = "Turkish",
|
||||
[0x0D] = "Chinese (simplified, Kantai)",
|
||||
[0x0E] = "Portuguese (Brazil)",
|
||||
[0x0F] = "Arabic",
|
||||
[0x10] = "Bulgarian",
|
||||
[0x11] = "Croatian",
|
||||
[0x12] = "Czech",
|
||||
[0x13] = "Danish",
|
||||
[0x14] = "Dutch",
|
||||
[0x15] = "Estonian",
|
||||
[0x16] = "Finnish",
|
||||
[0x17] = "Greek",
|
||||
[0x18] = "Hebrew",
|
||||
[0x19] = "Hindi",
|
||||
[0x1A] = "Hungarian",
|
||||
[0x1B] = "Latvian",
|
||||
[0x1C] = "Lithuanian",
|
||||
[0x1D] = "Norwegian",
|
||||
[0x1E] = "Polish",
|
||||
[0x1F] = "Romanian",
|
||||
[0x20] = "Serbian",
|
||||
[0x21] = "Slovak",
|
||||
[0x22] = "Slovenian",
|
||||
[0x23] = "Thai",
|
||||
[0x24] = "Ukrainian",
|
||||
[0x25] = "Vietnamese",
|
||||
},
|
||||
|
||||
// 0x62: Audio Speaker Volume
|
||||
[0x62] = new Dictionary<int, string>
|
||||
{
|
||||
[0x00] = "Mute",
|
||||
|
||||
// Other values are continuous
|
||||
},
|
||||
|
||||
// 0xDB: Image Mode (Dell monitors)
|
||||
[0xDB] = new Dictionary<int, string>
|
||||
{
|
||||
[0x00] = "Standard",
|
||||
[0x01] = "Multimedia",
|
||||
[0x02] = "Movie",
|
||||
[0x03] = "Game",
|
||||
[0x04] = "Sports",
|
||||
[0x05] = "Color Temperature",
|
||||
[0x06] = "Custom Color",
|
||||
[0x07] = "ComfortView",
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get human-readable name for a VCP value
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
|
||||
/// <param name="value">Value to translate</param>
|
||||
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
|
||||
public static string GetName(byte vcpCode, int value)
|
||||
{
|
||||
if (ValueNames.TryGetValue(vcpCode, out var codeValues))
|
||||
{
|
||||
if (codeValues.TryGetValue(value, out var name))
|
||||
{
|
||||
return $"{name} (0x{value:X2})";
|
||||
}
|
||||
}
|
||||
|
||||
return $"0x{value:X2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a VCP code has value name mappings
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code to check</param>
|
||||
/// <returns>True if value names are available</returns>
|
||||
public static bool HasValueNames(byte vcpCode) => ValueNames.ContainsKey(vcpCode);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ namespace PowerDisplay.Helpers
|
||||
public int Contrast { get; set; }
|
||||
|
||||
public int Volume { get; set; }
|
||||
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
}
|
||||
|
||||
public MonitorStateManager()
|
||||
@@ -147,6 +149,47 @@ namespace PowerDisplay.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor capabilities and schedule save.
|
||||
/// Capabilities are saved separately to avoid frequent writes.
|
||||
/// </summary>
|
||||
public void UpdateMonitorCapabilities(string hardwareId, string? capabilitiesRaw)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
Logger.LogWarning($"Cannot update capabilities: HardwareId is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Get or create state entry
|
||||
if (!_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
state = new MonitorState();
|
||||
_states[hardwareId] = state;
|
||||
}
|
||||
|
||||
// Update capabilities
|
||||
state.CapabilitiesRaw = capabilitiesRaw;
|
||||
|
||||
// Mark dirty and schedule save
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
// Schedule save
|
||||
_saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
|
||||
|
||||
Logger.LogInfo($"[State] Updated capabilities for monitor HardwareId='{hardwareId}' (length: {capabilitiesRaw?.Length ?? 0})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to update monitor capabilities: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get saved parameters for a monitor using HardwareId
|
||||
/// </summary>
|
||||
@@ -168,6 +211,27 @@ namespace PowerDisplay.Helpers
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get saved capabilities for a monitor using HardwareId
|
||||
/// </summary>
|
||||
public string? GetMonitorCapabilities(string hardwareId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
return state.CapabilitiesRaw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if state exists for a monitor (by HardwareId)
|
||||
/// </summary>
|
||||
@@ -215,6 +279,7 @@ namespace PowerDisplay.Helpers
|
||||
ColorTemperature = entry.ColorTemperature,
|
||||
Contrast = entry.Contrast,
|
||||
Volume = entry.Volume,
|
||||
CapabilitiesRaw = entry.CapabilitiesRaw,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -263,6 +328,7 @@ namespace PowerDisplay.Helpers
|
||||
ColorTemperature = state.ColorTemperature,
|
||||
Contrast = state.Contrast,
|
||||
Volume = state.Volume,
|
||||
CapabilitiesRaw = state.CapabilitiesRaw,
|
||||
LastUpdated = now,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor capabilities string
|
||||
/// Get monitor capabilities string with retry logic
|
||||
/// </summary>
|
||||
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -253,25 +253,62 @@ namespace PowerDisplay.Native.DDC
|
||||
|
||||
try
|
||||
{
|
||||
if (GetCapabilitiesStringLength(monitor.Handle, out uint length) && length > 0)
|
||||
// Step 1: Get capabilities string length (retry up to 3 times)
|
||||
uint length = 0;
|
||||
const int lengthMaxRetries = 3;
|
||||
for (int i = 0; i < lengthMaxRetries; i++)
|
||||
{
|
||||
if (GetCapabilitiesStringLength(monitor.Handle, out length) && length > 0)
|
||||
{
|
||||
Logger.LogDebug($"Got capabilities length: {length} (attempt {i + 1})");
|
||||
break;
|
||||
}
|
||||
|
||||
if (i < lengthMaxRetries - 1)
|
||||
{
|
||||
Thread.Sleep(100); // 100ms delay between retries
|
||||
}
|
||||
}
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
Logger.LogWarning("Failed to get capabilities string length after retries");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Step 2: Get actual capabilities string (retry up to 5 times)
|
||||
const int capsMaxRetries = 5;
|
||||
for (int i = 0; i < capsMaxRetries; i++)
|
||||
{
|
||||
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
|
||||
try
|
||||
{
|
||||
if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length))
|
||||
{
|
||||
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
|
||||
var capsString = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
Logger.LogInfo($"Got capabilities string (length: {capsString.Length}, attempt: {i + 1})");
|
||||
return capsString;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
|
||||
if (i < capsMaxRetries - 1)
|
||||
{
|
||||
Thread.Sleep(100); // 100ms delay between retries
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogWarning("Failed to get capabilities string after retries");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to get capabilities string: {ex.Message}");
|
||||
Logger.LogError($"Exception getting capabilities string: {ex.Message}");
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@@ -323,7 +360,7 @@ namespace PowerDisplay.Native.DDC
|
||||
|
||||
try
|
||||
{
|
||||
// Get all display devices with stable device IDs (Twinkle Tray style)
|
||||
// Get all display devices with stable device IDs
|
||||
var displayDevices = DdcCiNative.GetAllDisplayDevices();
|
||||
|
||||
// Also get hardware info for friendly names
|
||||
@@ -355,8 +392,7 @@ namespace PowerDisplay.Native.DDC
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sometimes Windows returns NULL handles. Implement Twinkle Tray's retry logic.
|
||||
// See: twinkle-tray/src/Monitors.js line 617
|
||||
// Sometimes Windows returns NULL handles, so we implement retry logic
|
||||
PHYSICAL_MONITOR[]? physicalMonitors = null;
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 200;
|
||||
@@ -380,7 +416,7 @@ namespace PowerDisplay.Native.DDC
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any handle is NULL (Twinkle Tray checks handleIsValid)
|
||||
// Check if any handle is NULL
|
||||
bool hasNullHandle = false;
|
||||
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||
{
|
||||
@@ -414,7 +450,7 @@ namespace PowerDisplay.Native.DDC
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match physical monitors with DisplayDeviceInfo (Twinkle Tray logic)
|
||||
// Match physical monitors with DisplayDeviceInfo
|
||||
// For each physical monitor on this adapter, find the corresponding DisplayDeviceInfo
|
||||
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||
{
|
||||
|
||||
@@ -394,8 +394,7 @@ namespace PowerDisplay.Native.DDC
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all display device information (using EnumDisplayDevices API)
|
||||
/// Implementation consistent with Twinkle Tray
|
||||
/// Get all display device information using EnumDisplayDevices API
|
||||
/// </summary>
|
||||
/// <returns>List of display device information</returns>
|
||||
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
|
||||
|
||||
@@ -12,11 +12,10 @@ namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages physical monitor handles - reuse, cleanup, and validation
|
||||
/// Twinkle Tray style handle management
|
||||
/// </summary>
|
||||
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||
{
|
||||
// Twinkle Tray style mapping: deviceKey -> physical handle
|
||||
// Mapping: deviceKey -> physical handle
|
||||
private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new();
|
||||
private bool _disposed;
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ namespace PowerDisplay.Native.DDC
|
||||
};
|
||||
|
||||
// VCP code priority order (for color temperature control)
|
||||
// Per MCCS specification:
|
||||
// - 0x0C (Color Temperature Request): Set specific color temperature preset
|
||||
// - 0x0B (Color Temperature Increment): Increment color temperature value
|
||||
private static readonly byte[] ColorTemperatureVcpCodes =
|
||||
{
|
||||
NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature
|
||||
NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment
|
||||
NativeConstants.VcpCodeSelectColorPreset, // 0x14 - Color preset selection
|
||||
NativeConstants.VcpCodeGamma, // 0x72 - Gamma correction
|
||||
NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature (primary)
|
||||
NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment (fallback)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -42,12 +42,20 @@ namespace PowerDisplay.Native
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature request (primary color temperature control)
|
||||
/// VCP code: Color Temperature Request (0x0C)
|
||||
/// Per MCCS v2.2a specification:
|
||||
/// - Used to SET and GET specific color temperature presets
|
||||
/// - Typically supports discrete values (e.g., 5000K, 6500K, 9300K)
|
||||
/// - Primary method for color temperature control
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperature = 0x0C;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature increment (incremental color temperature adjustment)
|
||||
/// VCP code: Color Temperature Increment (0x0B)
|
||||
/// Per MCCS v2.2a specification:
|
||||
/// - Used for incremental color temperature adjustment
|
||||
/// - Typically supports continuous range (0-100 or custom range)
|
||||
/// - Fallback method when 0x0C is not supported
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ namespace PowerDisplay.Serialization
|
||||
[JsonPropertyName("volume")]
|
||||
public int Volume { get; set; }
|
||||
|
||||
[JsonPropertyName("capabilitiesRaw")]
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
@@ -687,7 +687,18 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
communicationMethod: GetCommunicationMethodString(vm.Type),
|
||||
monitorType: vm.Type.ToString(),
|
||||
currentBrightness: vm.Brightness,
|
||||
colorTemperature: vm.ColorTemperature);
|
||||
colorTemperature: vm.ColorTemperature)
|
||||
{
|
||||
CapabilitiesRaw = vm.CapabilitiesRaw,
|
||||
VcpCodes = vm.VcpCapabilitiesInfo?.SupportedVcpCodes
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => $"0x{kvp.Key:X2}")
|
||||
.ToList() ?? new List<string>(),
|
||||
VcpCodesFormatted = vm.VcpCapabilitiesInfo?.SupportedVcpCodes
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => FormatVcpCodeForDisplay(kvp.Key, kvp.Value))
|
||||
.ToList() ?? new List<Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo>(),
|
||||
};
|
||||
|
||||
monitors.Add(monitorInfo);
|
||||
}
|
||||
@@ -712,8 +723,36 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get communication method string based on monitor type
|
||||
/// Format VCP code information for display in Settings UI
|
||||
/// </summary>
|
||||
private Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo FormatVcpCodeForDisplay(byte code, VcpCodeInfo info)
|
||||
{
|
||||
var result = new Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo
|
||||
{
|
||||
Title = $"{info.Name} (0x{code:X2})",
|
||||
};
|
||||
|
||||
if (info.IsContinuous)
|
||||
{
|
||||
result.Values = "Continuous range";
|
||||
result.HasValues = true;
|
||||
}
|
||||
else if (info.HasDiscreteValues)
|
||||
{
|
||||
var formattedValues = info.SupportedValues
|
||||
.Select(v => Core.Utils.VcpValueNames.GetName(code, v))
|
||||
.ToList();
|
||||
result.Values = $"Values: {string.Join(", ", formattedValues)}";
|
||||
result.HasValues = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.HasValues = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetCommunicationMethodString(MonitorType type)
|
||||
{
|
||||
return type switch
|
||||
|
||||
@@ -138,6 +138,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External";
|
||||
|
||||
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
|
||||
|
||||
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon glyph based on monitor type
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
|
||||
@@ -20,6 +22,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
private bool _enableColorTemperature;
|
||||
private bool _enableContrast;
|
||||
private bool _enableVolume;
|
||||
private string _capabilitiesRaw = string.Empty;
|
||||
private List<string> _vcpCodes = new List<string>();
|
||||
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
|
||||
|
||||
public MonitorInfo()
|
||||
{
|
||||
@@ -197,5 +202,70 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("capabilitiesRaw")]
|
||||
public string CapabilitiesRaw
|
||||
{
|
||||
get => _capabilitiesRaw;
|
||||
set
|
||||
{
|
||||
if (_capabilitiesRaw != value)
|
||||
{
|
||||
_capabilitiesRaw = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasCapabilities));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("vcpCodes")]
|
||||
public List<string> VcpCodes
|
||||
{
|
||||
get => _vcpCodes;
|
||||
set
|
||||
{
|
||||
if (_vcpCodes != value)
|
||||
{
|
||||
_vcpCodes = value ?? new List<string>();
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(VcpCodesSummary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("vcpCodesFormatted")]
|
||||
public List<VcpCodeDisplayInfo> VcpCodesFormatted
|
||||
{
|
||||
get => _vcpCodesFormatted;
|
||||
set
|
||||
{
|
||||
if (_vcpCodesFormatted != value)
|
||||
{
|
||||
_vcpCodesFormatted = value ?? new List<VcpCodeDisplayInfo>();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string VcpCodesSummary
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vcpCodes == null || _vcpCodes.Count == 0)
|
||||
{
|
||||
return "No VCP codes detected";
|
||||
}
|
||||
|
||||
var count = _vcpCodes.Count;
|
||||
var preview = string.Join(", ", _vcpCodes.Take(10));
|
||||
return count > 10
|
||||
? $"{count} VCP codes: {preview}..."
|
||||
: $"{count} VCP codes: {preview}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
@@ -31,5 +32,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int ColorTemperature { get; set; }
|
||||
|
||||
[JsonPropertyName("capabilitiesRaw")]
|
||||
public string CapabilitiesRaw { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vcpCodes")]
|
||||
public List<string> VcpCodes { get; set; } = new List<string>();
|
||||
|
||||
[JsonPropertyName("vcpCodesFormatted")]
|
||||
public List<VcpCodeDisplayInfo> VcpCodesFormatted { get; set; } = new List<VcpCodeDisplayInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
23
src/settings-ui/Settings.UI.Library/VcpCodeDisplayInfo.cs
Normal file
23
src/settings-ui/Settings.UI.Library/VcpCodeDisplayInfo.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Formatted VCP code display information
|
||||
/// </summary>
|
||||
public class VcpCodeDisplayInfo
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("values")]
|
||||
public string Values { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("hasValues")]
|
||||
public bool HasValues { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,48 @@
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<!-- VCP Capabilities -->
|
||||
<tkcontrols:SettingsCard
|
||||
Header="VCP Capabilities (Debug Info)"
|
||||
Description="DDC/CI VCP codes and supported values"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
Visibility="{x:Bind HasCapabilities, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Button
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="View VCP Details" />
|
||||
</ToolTipService.ToolTip>
|
||||
<Button.Flyout>
|
||||
<Flyout ShouldConstrainToRootBounds="False">
|
||||
<ScrollViewer MaxHeight="500" Width="450" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Detected VCP Codes" FontWeight="SemiBold" FontSize="13" />
|
||||
<ItemsControl ItemsSource="{x:Bind VcpCodesFormatted, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="library:VcpCodeDisplayInfo">
|
||||
<StackPanel Orientation="Vertical" Margin="0,4">
|
||||
<TextBlock Text="{x:Bind Title}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="11" />
|
||||
<TextBlock Text="{x:Bind Values}"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="12,2,0,0"
|
||||
Visibility="{x:Bind HasValues}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</DataTemplate>
|
||||
|
||||
Reference in New Issue
Block a user