mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
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:
204
doc/devdocs/modules/powerdisplay/MCCS_PARSER_DESIGN.md
Normal file
204
doc/devdocs/modules/powerdisplay/MCCS_PARSER_DESIGN.md
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user