This commit is contained in:
Yu Leng
2025-11-14 02:51:43 +08:00
parent 83410f1bc8
commit 4c799b61fc
19 changed files with 1232 additions and 22 deletions

View File

@@ -187,7 +187,7 @@ namespace PowerDisplay.Core.Models
public IntPtr Handle { get; set; } = IntPtr.Zero; public IntPtr Handle { get; set; } = IntPtr.Zero;
/// <summary> /// <summary>
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey) /// Device key - unique identifier part of device path
/// </summary> /// </summary>
public string DeviceKey { get; set; } = string.Empty; public string DeviceKey { get; set; } = string.Empty;
@@ -216,6 +216,16 @@ namespace PowerDisplay.Core.Models
/// </summary> /// </summary>
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None; 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> /// <summary>
/// Last update time /// Last update time
/// </summary> /// </summary>

View File

@@ -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";
}
}
}

View File

@@ -127,6 +127,36 @@ namespace PowerDisplay.Core
Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}"); 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); newMonitors.Add(monitor);
} }
} }

View File

@@ -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');
}
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -43,6 +43,8 @@ namespace PowerDisplay.Helpers
public int Contrast { get; set; } public int Contrast { get; set; }
public int Volume { get; set; } public int Volume { get; set; }
public string? CapabilitiesRaw { get; set; }
} }
public MonitorStateManager() 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> /// <summary>
/// Get saved parameters for a monitor using HardwareId /// Get saved parameters for a monitor using HardwareId
/// </summary> /// </summary>
@@ -168,6 +211,27 @@ namespace PowerDisplay.Helpers
return null; 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> /// <summary>
/// Check if state exists for a monitor (by HardwareId) /// Check if state exists for a monitor (by HardwareId)
/// </summary> /// </summary>
@@ -215,6 +279,7 @@ namespace PowerDisplay.Helpers
ColorTemperature = entry.ColorTemperature, ColorTemperature = entry.ColorTemperature,
Contrast = entry.Contrast, Contrast = entry.Contrast,
Volume = entry.Volume, Volume = entry.Volume,
CapabilitiesRaw = entry.CapabilitiesRaw,
}; };
} }
} }
@@ -263,6 +328,7 @@ namespace PowerDisplay.Helpers
ColorTemperature = state.ColorTemperature, ColorTemperature = state.ColorTemperature,
Contrast = state.Contrast, Contrast = state.Contrast,
Volume = state.Volume, Volume = state.Volume,
CapabilitiesRaw = state.CapabilitiesRaw,
LastUpdated = now, LastUpdated = now,
}; };
} }

View File

@@ -239,7 +239,7 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// Get monitor capabilities string /// Get monitor capabilities string with retry logic
/// </summary> /// </summary>
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default) public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
{ {
@@ -253,25 +253,62 @@ namespace PowerDisplay.Native.DDC
try 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); var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
try try
{ {
if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length)) 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 finally
{ {
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer); 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) catch (Exception ex)
{ {
Logger.LogWarning($"Failed to get capabilities string: {ex.Message}"); Logger.LogError($"Exception getting capabilities string: {ex.Message}");
} }
return string.Empty; return string.Empty;
@@ -323,7 +360,7 @@ namespace PowerDisplay.Native.DDC
try try
{ {
// Get all display devices with stable device IDs (Twinkle Tray style) // Get all display devices with stable device IDs
var displayDevices = DdcCiNative.GetAllDisplayDevices(); var displayDevices = DdcCiNative.GetAllDisplayDevices();
// Also get hardware info for friendly names // Also get hardware info for friendly names
@@ -355,8 +392,7 @@ namespace PowerDisplay.Native.DDC
continue; continue;
} }
// Sometimes Windows returns NULL handles. Implement Twinkle Tray's retry logic. // Sometimes Windows returns NULL handles, so we implement retry logic
// See: twinkle-tray/src/Monitors.js line 617
PHYSICAL_MONITOR[]? physicalMonitors = null; PHYSICAL_MONITOR[]? physicalMonitors = null;
const int maxRetries = 3; const int maxRetries = 3;
const int retryDelayMs = 200; const int retryDelayMs = 200;
@@ -380,7 +416,7 @@ namespace PowerDisplay.Native.DDC
continue; continue;
} }
// Check if any handle is NULL (Twinkle Tray checks handleIsValid) // Check if any handle is NULL
bool hasNullHandle = false; bool hasNullHandle = false;
for (int i = 0; i < physicalMonitors.Length; i++) for (int i = 0; i < physicalMonitors.Length; i++)
{ {
@@ -414,7 +450,7 @@ namespace PowerDisplay.Native.DDC
continue; 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 each physical monitor on this adapter, find the corresponding DisplayDeviceInfo
for (int i = 0; i < physicalMonitors.Length; i++) for (int i = 0; i < physicalMonitors.Length; i++)
{ {

View File

@@ -394,8 +394,7 @@ namespace PowerDisplay.Native.DDC
} }
/// <summary> /// <summary>
/// Get all display device information (using EnumDisplayDevices API) /// Get all display device information using EnumDisplayDevices API
/// Implementation consistent with Twinkle Tray
/// </summary> /// </summary>
/// <returns>List of display device information</returns> /// <returns>List of display device information</returns>
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices() public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()

View File

@@ -12,11 +12,10 @@ namespace PowerDisplay.Native.DDC
{ {
/// <summary> /// <summary>
/// Manages physical monitor handles - reuse, cleanup, and validation /// Manages physical monitor handles - reuse, cleanup, and validation
/// Twinkle Tray style handle management
/// </summary> /// </summary>
public partial class PhysicalMonitorHandleManager : IDisposable public partial class PhysicalMonitorHandleManager : IDisposable
{ {
// Twinkle Tray style mapping: deviceKey -> physical handle // Mapping: deviceKey -> physical handle
private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new(); private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new();
private bool _disposed; private bool _disposed;

View File

@@ -28,12 +28,13 @@ namespace PowerDisplay.Native.DDC
}; };
// VCP code priority order (for color temperature control) // 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 = private static readonly byte[] ColorTemperatureVcpCodes =
{ {
NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature (primary)
NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment (fallback)
NativeConstants.VcpCodeSelectColorPreset, // 0x14 - Color preset selection
NativeConstants.VcpCodeGamma, // 0x72 - Gamma correction
}; };
/// <summary> /// <summary>

View File

@@ -42,12 +42,20 @@ namespace PowerDisplay.Native
public const byte VcpCodeMute = 0x8D; public const byte VcpCodeMute = 0x8D;
/// <summary> /// <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> /// </summary>
public const byte VcpCodeColorTemperature = 0x0C; public const byte VcpCodeColorTemperature = 0x0C;
/// <summary> /// <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> /// </summary>
public const byte VcpCodeColorTemperatureIncrement = 0x0B; public const byte VcpCodeColorTemperatureIncrement = 0x0B;

View File

@@ -71,6 +71,9 @@ namespace PowerDisplay.Serialization
[JsonPropertyName("volume")] [JsonPropertyName("volume")]
public int Volume { get; set; } public int Volume { get; set; }
[JsonPropertyName("capabilitiesRaw")]
public string? CapabilitiesRaw { get; set; }
[JsonPropertyName("lastUpdated")] [JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; } public DateTime LastUpdated { get; set; }
} }

View File

@@ -687,7 +687,18 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
communicationMethod: GetCommunicationMethodString(vm.Type), communicationMethod: GetCommunicationMethodString(vm.Type),
monitorType: vm.Type.ToString(), monitorType: vm.Type.ToString(),
currentBrightness: vm.Brightness, 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); monitors.Add(monitorInfo);
} }
@@ -712,8 +723,36 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
} }
/// <summary> /// <summary>
/// Get communication method string based on monitor type /// Format VCP code information for display in Settings UI
/// </summary> /// </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) private string GetCommunicationMethodString(MonitorType type)
{ {
return type switch return type switch

View File

@@ -138,6 +138,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External"; public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External";
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
/// <summary> /// <summary>
/// Gets the icon glyph based on monitor type /// Gets the icon glyph based on monitor type
/// </summary> /// </summary>

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -20,6 +22,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
private bool _enableColorTemperature; private bool _enableColorTemperature;
private bool _enableContrast; private bool _enableContrast;
private bool _enableVolume; private bool _enableVolume;
private string _capabilitiesRaw = string.Empty;
private List<string> _vcpCodes = new List<string>();
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
public MonitorInfo() 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);
} }
} }

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library namespace Microsoft.PowerToys.Settings.UI.Library
@@ -31,5 +32,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("colorTemperature")] [JsonPropertyName("colorTemperature")]
public int ColorTemperature { get; set; } 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>();
} }
} }

View 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; }
}
}

View File

@@ -117,6 +117,48 @@
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor"> <tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor">
<ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" /> <ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<!-- VCP Capabilities -->
<tkcontrols:SettingsCard
Header="VCP Capabilities (Debug Info)"
Description="DDC/CI VCP codes and supported values"
HeaderIcon="{ui:FontIcon Glyph=&#xE946;}"
Visibility="{x:Bind HasCapabilities, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Button
Content="&#xE946;"
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.Items>
</tkcontrols:SettingsExpander> </tkcontrols:SettingsExpander>
</DataTemplate> </DataTemplate>