Replace regex parser with recursive descent parser

Introduce a new `MccsCapabilitiesParser` to replace the
regex-based `VcpCapabilitiesParser` for parsing MCCS capabilities
strings. The new parser uses a grammar-based recursive descent
approach, improving performance, extensibility, and error handling.

Key changes:
- Implement `MccsCapabilitiesParser` with `ref struct` for zero
  heap allocation and efficient parsing using `ReadOnlySpan<char>`.
- Add sub-parsers for `vcp()` and `vcpname()` segments.
- Accumulate errors in `ParseError` list and return partial results.
- Replace all references to `VcpCapabilitiesParser` with the new
  parser in `DdcCiNative.cs` and `MonitorManager.cs`.
- Enhance `VcpCapabilities` model with `MccsVersion` property.
- Add comprehensive unit tests in `MccsCapabilitiesParserTests.cs`
  to validate real-world examples and edge cases.
- Remove legacy `VcpCapabilitiesParser` and associated code.

Additional improvements:
- Optimize parsing for both space-separated and concatenated hex
  formats.
- Improve logging for parsing progress and error diagnostics.
- Adjust application initialization to prevent race conditions.
This commit is contained in:
Yu Leng
2025-12-03 01:32:06 +08:00
parent ea75725ba7
commit 9413b7cc37
8 changed files with 1497 additions and 326 deletions

View File

@@ -0,0 +1,204 @@
# MCCS Capabilities String Parser - Recursive Descent Design
## Overview
This document describes the recursive descent parser implementation for DDC/CI MCCS (Monitor Control Command Set) capabilities strings.
## Grammar Definition (BNF)
```bnf
capabilities ::= ['('] segment* [')']
segment ::= identifier '(' segment_content ')'
segment_content ::= text | vcp_entries | hex_list
vcp_entries ::= vcp_entry*
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
hex_list ::= hex_byte*
hex_byte ::= [0-9A-Fa-f]{2}
identifier ::= [a-z_A-Z]+
text ::= [^()]+
```
## Example Input
```
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 16 60(11 12 0F) DC DF)mccs_ver(2.2)vcpname(F0(Custom Setting)))
```
## Parser Architecture
### Component Hierarchy
```
MccsCapabilitiesParser (main parser)
├── ParseCapabilities() → MccsParseResult
├── ParseSegment() → ParsedSegment?
├── ParseBalancedContent() → string
├── ParseIdentifier() → ReadOnlySpan<char>
├── ApplySegment() → void
│ ├── ParseHexList() → List<byte>
│ ├── ParseVcpEntries() → Dictionary<byte, VcpCodeInfo>
│ └── ParseVcpNames() → void
├── VcpEntryParser (sub-parser for vcp() content)
│ └── TryParseEntry() → VcpEntry
└── VcpNameParser (sub-parser for vcpname() content)
└── TryParseEntry() → (byte code, string name)
```
### Design Principles
1. **ref struct for Zero Allocation**
- Main parser uses `ref struct` to avoid heap allocation
- Works with `ReadOnlySpan<char>` for efficient string slicing
- No intermediate string allocations during parsing
2. **Recursive Descent Pattern**
- Each grammar rule has a corresponding parse method
- Methods call each other recursively for nested structures
- Single-character lookahead via `Peek()`
3. **Error Recovery**
- Errors are accumulated, not thrown
- Parser attempts to continue after errors
- Returns partial results when possible
4. **Sub-parsers for Specialized Content**
- `VcpEntryParser` for VCP code entries
- `VcpNameParser` for custom VCP names
- Each sub-parser handles its own grammar subset
## Parse Methods Detail
### ParseCapabilities()
Entry point. Handles optional outer parentheses and iterates through segments.
```csharp
private MccsParseResult ParseCapabilities()
{
// Handle optional outer parens
// while (!IsAtEnd()) { ParseSegment() }
// Return result with accumulated errors
}
```
### ParseSegment()
Parses a single `identifier(content)` segment.
```csharp
private ParsedSegment? ParseSegment()
{
// 1. ParseIdentifier()
// 2. Expect '('
// 3. ParseBalancedContent()
// 4. Expect ')'
}
```
### ParseBalancedContent()
Extracts content between balanced parentheses, handling nested parens.
```csharp
private string ParseBalancedContent()
{
int depth = 1;
while (depth > 0) {
if (char == '(') depth++;
if (char == ')') depth--;
}
}
```
### ParseVcpEntries()
Delegates to `VcpEntryParser` for the specialized VCP entry grammar.
```csharp
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
Examples:
- "10" code=0x10, values=[]
- "14(04 05 06)" code=0x14, values=[4, 5, 6]
- "60(11 12 0F)" code=0x60, values=[0x11, 0x12, 0x0F]
```
## Comparison with Other Approaches
| Approach | Pros | Cons |
|----------|------|------|
| **Recursive Descent** (this) | Clear structure, handles nesting, extensible | More code |
| **Regex** (DDCSharp) | Concise | Hard to debug, limited nesting |
| **Mixed** (original) | Pragmatic | Inconsistent, hard to maintain |
## Performance Characteristics
- **Time Complexity**: O(n) where n = input length
- **Space Complexity**: O(1) for parsing + O(m) for output where m = number of VCP codes
- **Allocations**: Minimal - only for output structures
## Supported Segments
| Segment | Description | Parser |
|---------|-------------|--------|
| `prot(...)` | Protocol type | Direct assignment |
| `type(...)` | Display type (lcd/crt) | Direct assignment |
| `model(...)` | Model name | Direct assignment |
| `cmds(...)` | Supported commands | ParseHexList |
| `vcp(...)` | VCP code entries | VcpEntryParser |
| `mccs_ver(...)` | MCCS version | Direct assignment |
| `vcpname(...)` | Custom VCP names | VcpNameParser |
## Error Handling
```csharp
public readonly struct ParseError
{
public int Position { get; } // Character position
public string Message { get; } // Human-readable error
}
public sealed class MccsParseResult
{
public VcpCapabilities Capabilities { get; }
public IReadOnlyList<ParseError> Errors { get; }
public bool HasErrors => Errors.Count > 0;
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
}
```
## Usage Example
```csharp
// Parse capabilities string
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
if (result.IsValid)
{
var caps = result.Capabilities;
Console.WriteLine($"Model: {caps.Model}");
Console.WriteLine($"MCCS Version: {caps.MccsVersion}");
Console.WriteLine($"VCP Codes: {caps.SupportedVcpCodes.Count}");
}
if (result.HasErrors)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Parse error at {error.Position}: {error.Message}");
}
}
```
## Edge Cases Handled
1. **Missing outer parentheses** (Apple Cinema Display)
2. **No spaces between hex bytes** (`010203` vs `01 02 03`)
3. **Nested parentheses** in VCP values
4. **Unknown segments** (logged but not fatal)
5. **Malformed input** (partial results returned)
## Future Extensions
- Add `edid()` segment parsing
- Add `window()` segment parsing
- Add validation levels (VALID/USABLE/INVALID like ddcutil)
- Add source generation for AOT compatibility

View File

@@ -0,0 +1,587 @@
// 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.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Unit tests for MccsCapabilitiesParser class.
/// Tests parsing of DDC/CI MCCS capabilities strings using real-world examples.
/// Reference: https://www.ddcutil.com/cap_u3011_verbose_output/
/// </summary>
[TestClass]
public class MccsCapabilitiesParserTests
{
// Real capabilities string from Dell U3011 monitor
// Source: https://www.ddcutil.com/cap_u3011_verbose_output/
private const string DellU3011Capabilities =
"(prot(monitor)type(lcd)model(U3011)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 06 08 10 12 14(01 05 08 0B 0C) 16 18 1A 52 60(01 03 04 0C 0F 11 12) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 04 05) DF FD)mccs_ver(2.1)mswhql(1))";
// Real capabilities string from Dell P2416D monitor
private const string DellP2416DCapabilities =
"(prot(monitor)type(LCD)model(P2416D)cmds(01 02 03 07 0C E3 F3) vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 60(01 11 0F) AA(01 02) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 05) DF E0 E1 E2(00 01 02 04 0E 12 14 19) F0(00 08) F1(01 02) F2 FD) mswhql(1)asset_eep(40)mccs_ver(2.1))";
// Simple test string
private const string SimpleCapabilities =
"(prot(monitor)type(lcd)model(TestMonitor)vcp(10 12)mccs_ver(2.2))";
// Capabilities without outer parentheses (some monitors like Apple Cinema Display)
private const string NoOuterParensCapabilities =
"prot(monitor)type(lcd)model(TestMonitor)vcp(10 12)mccs_ver(2.0)";
// Concatenated hex format (no spaces between hex bytes)
private const string ConcatenatedHexCapabilities =
"(prot(monitor)cmds(01020307)vcp(101214)mccs_ver(2.1))";
[TestMethod]
public void Parse_NullInput_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(null);
// Assert
Assert.IsNotNull(result);
Assert.IsNotNull(result.Capabilities);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
Assert.IsFalse(result.HasErrors);
}
[TestMethod]
public void Parse_EmptyString_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(string.Empty);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_WhitespaceOnly_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(" \t\n ");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_DellU3011_ParsesProtocol()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
}
[TestMethod]
public void Parse_DellU3011_ParsesType()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("lcd", result.Capabilities.Type);
}
[TestMethod]
public void Parse_DellU3011_ParsesModel()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("U3011", result.Capabilities.Model);
}
[TestMethod]
public void Parse_DellU3011_ParsesMccsVersion()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("2.1", result.Capabilities.MccsVersion);
}
[TestMethod]
public void Parse_DellU3011_ParsesCommands()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
var cmds = result.Capabilities.SupportedCommands;
Assert.IsNotNull(cmds);
Assert.AreEqual(7, cmds.Count);
CollectionAssert.Contains(cmds, (byte)0x01);
CollectionAssert.Contains(cmds, (byte)0x02);
CollectionAssert.Contains(cmds, (byte)0x03);
CollectionAssert.Contains(cmds, (byte)0x07);
CollectionAssert.Contains(cmds, (byte)0x0C);
CollectionAssert.Contains(cmds, (byte)0xE3);
CollectionAssert.Contains(cmds, (byte)0xF3);
}
[TestMethod]
public void Parse_DellU3011_ParsesBrightnessVcpCode()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x10 is Brightness
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
var brightnessInfo = result.Capabilities.GetVcpCodeInfo(0x10);
Assert.IsNotNull(brightnessInfo);
Assert.AreEqual(0x10, brightnessInfo.Value.Code);
Assert.IsTrue(brightnessInfo.Value.IsContinuous);
}
[TestMethod]
public void Parse_DellU3011_ParsesContrastVcpCode()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x12 is Contrast
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_DellU3011_ParsesInputSourceWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x60 is Input Source with discrete values
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x60));
var inputSourceInfo = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsNotNull(inputSourceInfo);
Assert.IsTrue(inputSourceInfo.Value.HasDiscreteValues);
// Should have values: 01 03 04 0C 0F 11 12
var values = inputSourceInfo.Value.SupportedValues;
Assert.AreEqual(7, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x03));
Assert.IsTrue(values.Contains(0x04));
Assert.IsTrue(values.Contains(0x0C));
Assert.IsTrue(values.Contains(0x0F));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x12));
}
[TestMethod]
public void Parse_DellU3011_ParsesColorPresetWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x14 is Color Preset
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
var colorPresetInfo = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsNotNull(colorPresetInfo);
Assert.IsTrue(colorPresetInfo.Value.HasDiscreteValues);
// Should have values: 01 05 08 0B 0C
var values = colorPresetInfo.Value.SupportedValues;
Assert.AreEqual(5, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x05));
Assert.IsTrue(values.Contains(0x08));
Assert.IsTrue(values.Contains(0x0B));
Assert.IsTrue(values.Contains(0x0C));
}
[TestMethod]
public void Parse_DellU3011_ParsesPowerModeWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0xD6 is Power Mode
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xD6));
var powerModeInfo = result.Capabilities.GetVcpCodeInfo(0xD6);
Assert.IsNotNull(powerModeInfo);
Assert.IsTrue(powerModeInfo.Value.HasDiscreteValues);
// Should have values: 01 04 05
var values = powerModeInfo.Value.SupportedValues;
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x04));
Assert.IsTrue(values.Contains(0x05));
}
[TestMethod]
public void Parse_DellU3011_TotalVcpCodeCount()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP codes: 02 04 05 06 08 10 12 14 16 18 1A 52 60 AC AE B2 B6 C6 C8 C9 D6 DC DF FD
Assert.AreEqual(24, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_DellP2416D_ParsesModel()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert
Assert.AreEqual("P2416D", result.Capabilities.Model);
}
[TestMethod]
public void Parse_DellP2416D_ParsesTypeWithDifferentCase()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - Type is "LCD" (uppercase) in this monitor
Assert.AreEqual("LCD", result.Capabilities.Type);
}
[TestMethod]
public void Parse_DellP2416D_ParsesMccsVersion()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert
Assert.AreEqual("2.1", result.Capabilities.MccsVersion);
}
[TestMethod]
public void Parse_DellP2416D_ParsesInputSourceWithThreeValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - VCP 0x60 Input Source has values: 01 11 0F
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x60));
var inputSourceInfo = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsNotNull(inputSourceInfo);
var values = inputSourceInfo.Value.SupportedValues;
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x0F));
}
[TestMethod]
public void Parse_DellP2416D_ParsesE2WithManyValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - VCP 0xE2 has values: 00 01 02 04 0E 12 14 19
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xE2));
var e2Info = result.Capabilities.GetVcpCodeInfo(0xE2);
Assert.IsNotNull(e2Info);
var values = e2Info.Value.SupportedValues;
Assert.AreEqual(8, values.Count);
}
[TestMethod]
public void Parse_NoOuterParentheses_StillParses()
{
// Act - Some monitors like Apple Cinema Display omit outer parens
var result = MccsCapabilitiesParser.Parse(NoOuterParensCapabilities);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual("TestMonitor", result.Capabilities.Model);
Assert.AreEqual("2.0", result.Capabilities.MccsVersion);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_ConcatenatedHexFormat_ParsesCorrectly()
{
// Act - Some monitors output hex without spaces: cmds(01020307)
var result = MccsCapabilitiesParser.Parse(ConcatenatedHexCapabilities);
// Assert
var cmds = result.Capabilities.SupportedCommands;
Assert.AreEqual(4, cmds.Count);
CollectionAssert.Contains(cmds, (byte)0x01);
CollectionAssert.Contains(cmds, (byte)0x02);
CollectionAssert.Contains(cmds, (byte)0x03);
CollectionAssert.Contains(cmds, (byte)0x07);
// VCP codes without spaces
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
}
[TestMethod]
public void Parse_NestedParenthesesInVcp_HandlesCorrectly()
{
// Arrange - VCP code 0x14 with nested discrete values
var input = "(vcp(14(01 05 08)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
var vcpInfo = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsNotNull(vcpInfo);
Assert.AreEqual(3, vcpInfo.Value.SupportedValues.Count);
}
[TestMethod]
public void Parse_MultipleVcpCodesWithMixedFormats_ParsesAll()
{
// Arrange - Mixed: some with values, some without
var input = "(vcp(10 12 14(01 05) 16 60(0F 11)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual(5, result.Capabilities.SupportedVcpCodes.Count);
// Continuous codes (no discrete values)
var brightness = result.Capabilities.GetVcpCodeInfo(0x10);
Assert.IsTrue(brightness?.IsContinuous ?? false);
var contrast = result.Capabilities.GetVcpCodeInfo(0x12);
Assert.IsTrue(contrast?.IsContinuous ?? false);
// Discrete codes (with values)
var colorPreset = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsTrue(colorPreset?.HasDiscreteValues ?? false);
Assert.AreEqual(2, colorPreset?.SupportedValues.Count);
var inputSource = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsTrue(inputSource?.HasDiscreteValues ?? false);
Assert.AreEqual(2, inputSource?.SupportedValues.Count);
}
[TestMethod]
public void Parse_UnknownSegments_DoesNotFail()
{
// Arrange - Contains unknown segments like mswhql and asset_eep
var input = "(prot(monitor)mswhql(1)asset_eep(40)vcp(10))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsFalse(result.HasErrors);
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
}
[TestMethod]
public void Parse_ExtraWhitespace_HandlesCorrectly()
{
// Arrange - Extra spaces everywhere
var input = "( prot( monitor ) type( lcd ) vcp( 10 12 14( 01 05 ) ) )";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual(3, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_LowercaseHex_ParsesCorrectly()
{
// Arrange - All lowercase hex
var input = "(cmds(01 0c e3 f3)vcp(10 ac ae))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
CollectionAssert.Contains(result.Capabilities.SupportedCommands, (byte)0xE3);
CollectionAssert.Contains(result.Capabilities.SupportedCommands, (byte)0xF3);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAC));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAE));
}
[TestMethod]
public void Parse_MixedCaseHex_ParsesCorrectly()
{
// Arrange - Mixed case hex
var input = "(vcp(Aa Bb cC Dd))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAA));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xBB));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xCC));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xDD));
}
[TestMethod]
public void Parse_MalformedInput_ReturnsPartialResults()
{
// Arrange - Missing closing paren for vcp section
var input = "(prot(monitor)vcp(10 12";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should still parse what it can
Assert.AreEqual("monitor", result.Capabilities.Protocol);
// VCP codes should still be parsed
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_InvalidHexInVcp_SkipsAndContinues()
{
// Arrange - Contains invalid hex "GG"
var input = "(vcp(10 GG 12 14))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should skip invalid and parse valid codes
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
Assert.AreEqual(3, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_SingleCharacterHex_Skipped()
{
// Arrange - Single char "A" is not valid (need 2 chars)
var input = "(vcp(10 A 12))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should only have 10 and 12
Assert.AreEqual(2, result.Capabilities.SupportedVcpCodes.Count);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void GetVcpCodesAsHexStrings_ReturnsSortedList()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60 10 14 12))");
// Act
var hexStrings = result.Capabilities.GetVcpCodesAsHexStrings();
// Assert - Should be sorted
Assert.AreEqual(4, hexStrings.Count);
Assert.AreEqual("0x10", hexStrings[0]);
Assert.AreEqual("0x12", hexStrings[1]);
Assert.AreEqual("0x14", hexStrings[2]);
Assert.AreEqual("0x60", hexStrings[3]);
}
[TestMethod]
public void GetSortedVcpCodes_ReturnsSortedEnumerable()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60 10 14 12))");
// Act
var sortedCodes = result.Capabilities.GetSortedVcpCodes().ToList();
// Assert
Assert.AreEqual(0x10, sortedCodes[0].Code);
Assert.AreEqual(0x12, sortedCodes[1].Code);
Assert.AreEqual(0x14, sortedCodes[2].Code);
Assert.AreEqual(0x60, sortedCodes[3].Code);
}
[TestMethod]
public void HasDiscreteValues_ContinuousCode_ReturnsFalse()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(10))");
// Act & Assert
Assert.IsFalse(result.Capabilities.HasDiscreteValues(0x10));
}
[TestMethod]
public void HasDiscreteValues_DiscreteCode_ReturnsTrue()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60(01 11)))");
// Act & Assert
Assert.IsTrue(result.Capabilities.HasDiscreteValues(0x60));
}
[TestMethod]
public void GetSupportedValues_DiscreteCode_ReturnsValues()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60(01 11 0F)))");
// Act
var values = result.Capabilities.GetSupportedValues(0x60);
// Assert
Assert.IsNotNull(values);
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x0F));
}
[TestMethod]
public void IsValid_ValidCapabilities_ReturnsTrue()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.IsTrue(result.IsValid);
Assert.IsFalse(result.HasErrors);
}
[TestMethod]
public void IsValid_EmptyVcpCodes_ReturnsFalse()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse("(prot(monitor)type(lcd))");
// Assert - No VCP codes = not valid
Assert.IsFalse(result.IsValid);
}
[TestMethod]
public void Capabilities_RawProperty_ContainsOriginalString()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse(SimpleCapabilities);
// Assert
Assert.AreEqual(SimpleCapabilities, result.Capabilities.Raw);
}
}

View File

@@ -238,7 +238,8 @@ namespace PowerDisplay.Common.Drivers.DDC
}
// Parse the capabilities string
var capabilities = Utils.VcpCapabilitiesParser.Parse(capsString);
var parseResult = Utils.MccsCapabilitiesParser.Parse(capsString);
var capabilities = parseResult.Capabilities;
if (capabilities == null || capabilities.SupportedVcpCodes.Count == 0)
{
Logger.LogDebug($"FetchCapabilities: Failed to parse capabilities string for handle 0x{hPhysicalMonitor:X}");

View File

@@ -32,6 +32,11 @@ namespace PowerDisplay.Common.Models
/// </summary>
public string? Protocol { get; set; }
/// <summary>
/// MCCS version (e.g., "2.2", "2.1")
/// </summary>
public string? MccsVersion { get; set; }
/// <summary>
/// Supported command codes
/// </summary>

View File

@@ -0,0 +1,661 @@
// 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.Runtime.CompilerServices;
using ManagedCommon;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Recursive descent parser for DDC/CI MCCS capabilities strings.
///
/// MCCS Capabilities String Grammar (BNF):
/// <code>
/// capabilities ::= '(' segment* ')'
/// segment ::= identifier '(' segment_content ')'
/// segment_content ::= text | vcp_entries | hex_list
/// vcp_entries ::= vcp_entry*
/// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
/// hex_list ::= hex_byte*
/// hex_byte ::= [0-9A-Fa-f]{2}
/// identifier ::= [a-z_]+
/// text ::= [^()]+
/// </code>
///
/// Example input:
/// (prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03)vcp(10 12 14(04 05) 60(11 12))mccs_ver(2.2))
/// </summary>
public ref struct MccsCapabilitiesParser
{
private readonly List<ParseError> _errors;
private ReadOnlySpan<char> _input;
private int _position;
/// <summary>
/// Parse a capabilities string into structured VcpCapabilities.
/// </summary>
/// <param name="capabilitiesString">Raw MCCS capabilities string</param>
/// <returns>Parsed capabilities object with any parse errors</returns>
public static MccsParseResult Parse(string? capabilitiesString)
{
if (string.IsNullOrWhiteSpace(capabilitiesString))
{
return new MccsParseResult(VcpCapabilities.Empty, new List<ParseError>());
}
var parser = new MccsCapabilitiesParser(capabilitiesString);
return parser.ParseCapabilities();
}
private MccsCapabilitiesParser(string input)
{
_input = input.AsSpan();
_position = 0;
_errors = new List<ParseError>();
}
/// <summary>
/// Main entry point: parse the entire capabilities string.
/// capabilities ::= '(' segment* ')' | segment*
/// </summary>
private MccsParseResult ParseCapabilities()
{
var capabilities = new VcpCapabilities
{
Raw = _input.ToString(),
};
SkipWhitespace();
// Handle optional outer parentheses (some monitors omit them)
bool hasOuterParens = Peek() == '(';
if (hasOuterParens)
{
Advance(); // consume '('
}
// Parse segments until end or closing paren
while (!IsAtEnd())
{
SkipWhitespace();
if (IsAtEnd())
{
break;
}
if (Peek() == ')')
{
if (hasOuterParens)
{
Advance(); // consume closing ')'
}
break;
}
// Parse a segment: identifier(content)
var segment = ParseSegment();
if (segment.HasValue)
{
ApplySegment(capabilities, segment.Value);
}
}
return new MccsParseResult(capabilities, _errors);
}
/// <summary>
/// Parse a single segment: identifier '(' content ')'
/// </summary>
private ParsedSegment? ParseSegment()
{
SkipWhitespace();
int startPos = _position;
// Parse identifier
var identifier = ParseIdentifier();
if (identifier.IsEmpty)
{
// Not a valid segment start - skip this character and continue
if (!IsAtEnd())
{
Advance();
}
return null;
}
SkipWhitespace();
// Expect '('
if (Peek() != '(')
{
AddError($"Expected '(' after identifier '{identifier.ToString()}' at position {_position}");
return null;
}
Advance(); // consume '('
// Parse content until matching ')'
var content = ParseBalancedContent();
// Expect ')'
if (Peek() != ')')
{
AddError($"Expected ')' to close segment '{identifier.ToString()}' at position {_position}");
}
else
{
Advance(); // consume ')'
}
return new ParsedSegment(identifier.ToString(), content);
}
/// <summary>
/// Parse content between balanced parentheses.
/// Handles nested parentheses correctly.
/// </summary>
private string ParseBalancedContent()
{
int start = _position;
int depth = 1;
while (!IsAtEnd() && depth > 0)
{
char c = Peek();
if (c == '(')
{
depth++;
}
else if (c == ')')
{
depth--;
if (depth == 0)
{
break; // Don't consume the closing paren
}
}
Advance();
}
return _input.Slice(start, _position - start).ToString();
}
/// <summary>
/// Parse an identifier (lowercase letters and underscores).
/// identifier ::= [a-z_]+
/// </summary>
private ReadOnlySpan<char> ParseIdentifier()
{
int start = _position;
while (!IsAtEnd() && IsIdentifierChar(Peek()))
{
Advance();
}
return _input.Slice(start, _position - start);
}
/// <summary>
/// Apply a parsed segment to the capabilities object.
/// </summary>
private void ApplySegment(VcpCapabilities capabilities, ParsedSegment segment)
{
switch (segment.Name.ToLowerInvariant())
{
case "prot":
capabilities.Protocol = segment.Content.Trim();
break;
case "type":
capabilities.Type = segment.Content.Trim();
break;
case "model":
capabilities.Model = segment.Content.Trim();
break;
case "mccs_ver":
capabilities.MccsVersion = segment.Content.Trim();
break;
case "cmds":
capabilities.SupportedCommands = ParseHexList(segment.Content);
break;
case "vcp":
capabilities.SupportedVcpCodes = ParseVcpEntries(segment.Content);
break;
case "vcpname":
ParseVcpNames(segment.Content, capabilities);
break;
default:
// Store unknown segments for potential future use
Logger.LogDebug($"Unknown capabilities segment: {segment.Name}({segment.Content})");
break;
}
}
/// <summary>
/// Parse VCP entries: vcp_entry*
/// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
/// </summary>
private Dictionary<byte, VcpCodeInfo> ParseVcpEntries(string content)
{
var vcpCodes = new Dictionary<byte, VcpCodeInfo>();
var parser = new VcpEntryParser(content);
while (parser.TryParseEntry(out var entry))
{
var name = VcpCodeNames.GetName(entry.Code);
vcpCodes[entry.Code] = new VcpCodeInfo(entry.Code, name, entry.Values);
}
return vcpCodes;
}
/// <summary>
/// Parse a hex byte list: hex_byte*
/// Handles both space-separated (01 02 03) and concatenated (010203) formats.
/// </summary>
private static List<byte> ParseHexList(string content)
{
var result = new List<byte>();
var span = content.AsSpan();
int i = 0;
while (i < span.Length)
{
// Skip whitespace
while (i < span.Length && char.IsWhiteSpace(span[i]))
{
i++;
}
if (i >= span.Length)
{
break;
}
// Try to read two hex digits
if (i + 1 < span.Length && IsHexDigit(span[i]) && IsHexDigit(span[i + 1]))
{
if (byte.TryParse(span.Slice(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
{
result.Add(value);
}
i += 2;
}
else
{
i++; // Skip invalid character
}
}
return result;
}
/// <summary>
/// Parse vcpname entries: hex_byte '(' name ')'
/// </summary>
private void ParseVcpNames(string content, VcpCapabilities capabilities)
{
// vcpname format: F0(Custom Name 1) F1(Custom Name 2)
var parser = new VcpNameParser(content);
while (parser.TryParseEntry(out var code, out var name))
{
if (capabilities.SupportedVcpCodes.TryGetValue(code, out var existingInfo))
{
// Update existing entry with custom name
capabilities.SupportedVcpCodes[code] = new VcpCodeInfo(code, name, existingInfo.SupportedValues);
}
else
{
// Add new entry with custom name
capabilities.SupportedVcpCodes[code] = new VcpCodeInfo(code, name, Array.Empty<int>());
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _input[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Advance() => _position++;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _input.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
Advance();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIdentifierChar(char c) =>
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
private void AddError(string message)
{
_errors.Add(new ParseError(_position, message));
Logger.LogWarning($"[MccsParser] {message}");
}
}
/// <summary>
/// Sub-parser for VCP entries within the vcp() segment.
/// </summary>
internal ref struct VcpEntryParser
{
private ReadOnlySpan<char> _content;
private int _position;
public VcpEntryParser(string content)
{
_content = content.AsSpan();
_position = 0;
}
/// <summary>
/// Try to parse the next VCP entry.
/// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
/// </summary>
public bool TryParseEntry(out VcpEntry entry)
{
entry = default;
SkipWhitespace();
if (IsAtEnd())
{
return false;
}
// Parse hex byte (VCP code)
if (!TryParseHexByte(out var code))
{
// Skip invalid character and try again
_position++;
return TryParseEntry(out entry);
}
var values = new List<int>();
SkipWhitespace();
// Check for optional value list
if (!IsAtEnd() && Peek() == '(')
{
_position++; // consume '('
// Parse values until ')'
while (!IsAtEnd() && Peek() != ')')
{
SkipWhitespace();
if (Peek() == ')')
{
break;
}
if (TryParseHexByte(out var value))
{
values.Add(value);
}
else
{
_position++; // Skip invalid character
}
}
if (!IsAtEnd() && Peek() == ')')
{
_position++; // consume ')'
}
}
entry = new VcpEntry(code, values);
return true;
}
private bool TryParseHexByte(out byte value)
{
value = 0;
if (_position + 1 >= _content.Length)
{
return false;
}
if (!IsHexDigit(_content[_position]) || !IsHexDigit(_content[_position + 1]))
{
return false;
}
if (byte.TryParse(_content.Slice(_position, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
{
_position += 2;
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _content[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _content.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
_position++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
/// <summary>
/// Sub-parser for vcpname entries.
/// </summary>
internal ref struct VcpNameParser
{
private ReadOnlySpan<char> _content;
private int _position;
public VcpNameParser(string content)
{
_content = content.AsSpan();
_position = 0;
}
/// <summary>
/// Try to parse the next vcpname entry.
/// vcpname_entry ::= hex_byte '(' name ')'
/// </summary>
public bool TryParseEntry(out byte code, out string name)
{
code = 0;
name = string.Empty;
SkipWhitespace();
if (IsAtEnd())
{
return false;
}
// Parse hex byte
if (!TryParseHexByte(out code))
{
_position++;
return TryParseEntry(out code, out name);
}
SkipWhitespace();
// Expect '('
if (IsAtEnd() || Peek() != '(')
{
return false;
}
_position++; // consume '('
// Parse name until ')'
int start = _position;
while (!IsAtEnd() && Peek() != ')')
{
_position++;
}
name = _content.Slice(start, _position - start).ToString().Trim();
if (!IsAtEnd() && Peek() == ')')
{
_position++; // consume ')'
}
return true;
}
private bool TryParseHexByte(out byte value)
{
value = 0;
if (_position + 1 >= _content.Length)
{
return false;
}
if (!IsHexDigit(_content[_position]) || !IsHexDigit(_content[_position + 1]))
{
return false;
}
if (byte.TryParse(_content.Slice(_position, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
{
_position += 2;
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _content[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _content.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
_position++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
/// <summary>
/// Represents a parsed segment from the capabilities string.
/// </summary>
internal readonly struct ParsedSegment
{
public string Name { get; }
public string Content { get; }
public ParsedSegment(string name, string content)
{
Name = name;
Content = content;
}
}
/// <summary>
/// Represents a parsed VCP entry.
/// </summary>
internal readonly struct VcpEntry
{
public byte Code { get; }
public IReadOnlyList<int> Values { get; }
public VcpEntry(byte code, IReadOnlyList<int> values)
{
Code = code;
Values = values;
}
}
/// <summary>
/// Represents a parse error with position information.
/// </summary>
public readonly struct ParseError
{
public int Position { get; }
public string Message { get; }
public ParseError(int position, string message)
{
Position = position;
Message = message;
}
public override string ToString() => $"[{Position}] {Message}";
}
/// <summary>
/// Result of parsing MCCS capabilities string.
/// </summary>
public sealed class MccsParseResult
{
public VcpCapabilities Capabilities { get; }
public IReadOnlyList<ParseError> Errors { get; }
public bool HasErrors => Errors.Count > 0;
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
public MccsParseResult(VcpCapabilities capabilities, IReadOnlyList<ParseError> errors)
{
Capabilities = capabilities;
Errors = errors;
}
}
}

View File

@@ -1,315 +0,0 @@
// 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.Text;
using System.Text.RegularExpressions;
using ManagedCommon;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.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 = new StringBuilder();
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.Append(ch);
}
return result.ToString();
}
/// <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 = new StringBuilder();
for (int i = startIndex; i < section.Length && depth > 0; i++)
{
var ch = section[i];
if (ch == '(')
{
depth++;
result.Append(ch);
}
else if (ch == ')')
{
depth--;
if (depth == 0)
{
break;
}
result.Append(ch);
}
else
{
result.Append(ch);
}
}
return result.ToString();
}
/// <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

@@ -242,7 +242,16 @@ namespace PowerDisplay.Core
if (!string.IsNullOrEmpty(capsString))
{
monitor.CapabilitiesRaw = capsString;
monitor.VcpCapabilitiesInfo = Common.Utils.VcpCapabilitiesParser.Parse(capsString);
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}");
}
}
Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes");

View File

@@ -112,10 +112,6 @@ namespace PowerDisplay
RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature");
RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile");
// Signal that process is ready to receive events
// This allows the C++ module to wait for initialization instead of using hardcoded Sleep
SignalProcessReady();
// Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0)
{
@@ -152,6 +148,9 @@ namespace PowerDisplay
// Standalone mode - activate and show window immediately
_mainWindow.Activate();
Logger.LogInfo("Window activated (standalone mode)");
// Signal ready immediately in standalone mode
SignalProcessReady();
}
else
{
@@ -159,20 +158,40 @@ namespace PowerDisplay
Logger.LogInfo("Window created, waiting for show event (PowerToys mode)");
// Start background initialization to scan monitors even when hidden
// Signal process ready AFTER initialization completes to prevent race condition
_ = Task.Run(async () =>
{
// Give window a moment to finish construction
await Task.Delay(500);
await Task.Delay(100);
// Trigger initialization on UI thread
// Trigger initialization on UI thread and wait for completion
var initComplete = new TaskCompletionSource<bool>();
_mainWindow?.DispatcherQueue.TryEnqueue(async () =>
{
if (_mainWindow is MainWindow mainWindow)
try
{
await mainWindow.EnsureInitializedAsync();
Logger.LogInfo("Background initialization completed");
if (_mainWindow is MainWindow mainWindow)
{
await mainWindow.EnsureInitializedAsync();
Logger.LogInfo("Background initialization completed");
}
initComplete.SetResult(true);
}
catch (Exception ex)
{
Logger.LogError($"Background initialization failed: {ex.Message}");
initComplete.SetResult(false);
}
});
// Wait for initialization to complete before signaling ready
await initComplete.Task;
// NOW signal that process is ready to receive events
// This ensures window is fully initialized before C++ module can send Toggle/Show events
SignalProcessReady();
Logger.LogInfo("Process ready signal sent after initialization");
});
}
}