From 12d898d7fdbc2cab9ef54a7e42cab9ccb185836e Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:13:26 -0600 Subject: [PATCH 01/17] Add toggle command to switch between two input sources; update documentation and examples --- CHANGELOG.md | 5 + DDCSwitch/Commands/CommandRouter.cs | 1 + DDCSwitch/Commands/HelpCommand.cs | 5 + DDCSwitch/Commands/ToggleCommand.cs | 401 ++++++++++++++++++++++++++++ DDCSwitch/JsonContext.cs | 10 + EXAMPLES.md | 58 ++++ README.md | 28 ++ 7 files changed, 508 insertions(+) create mode 100644 DDCSwitch/Commands/ToggleCommand.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 392330f..ebb9193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to ddcswitch will be documented in this file. +## [1.0.3] - 2026-01-08 + +### Added +- Added the toggle command to flip a monitor between two input sources + ## [1.0.2] - 2026-01-07 ### Added diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index e9761c3..0cefadb 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -37,6 +37,7 @@ public static int Route(string[] args) "list" or "ls" => ListCommand.Execute(jsonOutput, verboseOutput), "get" => GetCommand.Execute(filteredArgs, jsonOutput), "set" => SetCommand.Execute(filteredArgs, jsonOutput), + "toggle" => ToggleCommand.Execute(filteredArgs, jsonOutput), "version" or "-v" or "--version" => HelpCommand.ShowVersion(jsonOutput), "help" or "-h" or "--help" or "/?" => HelpCommand.ShowUsage(), _ => InvalidCommand(filteredArgs[0], jsonOutput) diff --git a/DDCSwitch/Commands/HelpCommand.cs b/DDCSwitch/Commands/HelpCommand.cs index 54895b0..5b2697c 100644 --- a/DDCSwitch/Commands/HelpCommand.cs +++ b/DDCSwitch/Commands/HelpCommand.cs @@ -58,6 +58,9 @@ public static int ShowUsage() commandsTable.AddRow( "[cyan]set[/] [green][/] [blue][/] [magenta][/]", "Set value for a monitor feature"); + commandsTable.AddRow( + "[cyan]toggle[/] [green][/] [blue][/] [blue][/]", + "Toggle between two input sources automatically"); commandsTable.AddRow( "[cyan]version[/] [dim]or[/] [cyan]-v[/]", "Display version information"); @@ -90,6 +93,8 @@ public static int ShowUsage() AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch get 0 brightness"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 0 input HDMI1"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 1 brightness 75%"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle 0 HDMI1 DP1"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle \"Dell Monitor\" HDMI1 HDMI2"); return 0; } diff --git a/DDCSwitch/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs new file mode 100644 index 0000000..e3dc3d3 --- /dev/null +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -0,0 +1,401 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class ToggleCommand +{ + public static int Execute(string[] args, bool jsonOutput) + { + // Require exactly 4 arguments: toggle + if (args.Length < 4) + { + return HandleMissingArguments(args.Length, jsonOutput); + } + + // Extract arguments + string monitorArg = args[1]; + string input1Arg = args[2]; + string input2Arg = args[3]; + + // Validate input sources and get their values + if (!InputSource.TryParse(input1Arg, out uint input1Value)) + { + return HandleInvalidInput(input1Arg, "first", jsonOutput); + } + + if (!InputSource.TryParse(input2Arg, out uint input2Value)) + { + return HandleInvalidInput(input2Arg, "second", jsonOutput); + } + + // Validate that inputs are different + if (input1Value == input2Value) + { + return HandleIdenticalInputs(input1Arg, input2Arg, jsonOutput); + } + + // Enumerate monitors + var monitors = MonitorController.EnumerateMonitors(); + + if (monitors.Count == 0) + { + return HandleNoMonitors(jsonOutput); + } + + // Find the specified monitor + var monitor = MonitorController.FindMonitor(monitors, monitorArg); + + if (monitor == null) + { + return HandleMonitorNotFound(monitors, monitorArg, jsonOutput); + } + + // Execute the toggle operation + int result = ExecuteToggle(monitor, input1Value, input2Value, input1Arg, input2Arg, jsonOutput); + + // Cleanup resources + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static int HandleMissingArguments(int actualCount, bool jsonOutput) + { + string errorMessage = actualCount switch + { + 0 => "Command 'toggle' requires monitor and two input sources", + 1 => "Monitor and two input sources required", + 2 => "Two input sources required", + 3 => "Second input source required", + _ => "Invalid number of arguments" + }; + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine("Usage: [yellow]ddcswitch toggle [/]"); + } + + return 1; + } + + + + private static int HandleInvalidInput(string invalidInput, string position, bool jsonOutput) + { + string errorMessage = $"Invalid {position} input source: '{invalidInput}'"; + string suggestion = "Valid input sources include: HDMI1, HDMI2, DisplayPort1, DisplayPort2, DVI1, DVI2, VGA1, VGA2, or numeric codes (e.g., 0x11, 17)"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, $"{errorMessage}. {suggestion}"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine($"[dim]{suggestion}[/]"); + } + + return 1; + } + + private static int HandleIdenticalInputs(string input1, string input2, bool jsonOutput) + { + string errorMessage = $"Input sources must be different. Both inputs resolve to the same source: '{input1}' and '{input2}'"; + string suggestion = "Please specify two different input sources to toggle between"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, $"{errorMessage}. {suggestion}"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine($"[dim]{suggestion}[/]"); + } + + return 1; + } + + /// + /// Detects the current input source from the monitor using VCP code 0x60 + /// + /// Monitor to read current input from + /// Whether to format output as JSON + /// The detected current input value + /// 0 on success, 1 on failure + private static int DetectCurrentInput(Monitor monitor, bool jsonOutput, out uint currentInput) + { + currentInput = 0; + + // Try to read current input source using VCP code 0x60 + if (monitor.TryGetVcpFeature(InputSource.VcpInputSource, out currentInput, out uint maxValue, out int errorCode)) + { + // Successfully detected current input + return 0; + } + + // Handle detection failure + return HandleInputDetectionFailure(monitor, errorCode, jsonOutput); + } + + private static int HandleInputDetectionFailure(Monitor monitor, int errorCode, bool jsonOutput) + { + string errorMessage = errorCode switch + { + 0x00000006 => "Monitor handle is invalid", // ERROR_INVALID_HANDLE + 0x00000057 => "Monitor does not support input source reading", // ERROR_INVALID_PARAMETER + 0x00000102 => "Monitor communication timeout", // WAIT_TIMEOUT + _ => $"Failed to read current input source (Error: 0x{errorCode:X8})" + }; + + string fallbackMessage = "Will use fallback behavior and switch to first input source"; + string fullMessage = $"{errorMessage}. {fallbackMessage}"; + + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, fullMessage, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + string escapedMonitorName = monitor.Name.Replace("[", "[[").Replace("]", "]]"); + ConsoleOutputFormatter.WriteError($"Monitor [[{monitor.Index}]] {escapedMonitorName}: {errorMessage}"); + AnsiConsole.MarkupLine($"[yellow]Warning:[/] {fallbackMessage}"); + } + + // Return 2 to indicate a warning condition that allows fallback behavior + // This is different from return 1 which indicates a hard error + return 2; + } + + /// + /// Determines which input to switch to based on current input and the two specified inputs + /// + /// The currently active input source + /// First input source value + /// Second input source value + /// The target input value to switch to + private static uint DetermineTargetInput(uint currentInput, uint input1Value, uint input2Value) + { + // If current input matches input1, switch to input2 + if (currentInput == input1Value) + { + return input2Value; + } + + // If current input matches input2, switch to input1 + if (currentInput == input2Value) + { + return input1Value; + } + + // If current input is neither specified input, default to input1 + // This handles the edge case where the current input is something else entirely + return input1Value; + } + + private static int HandleNoMonitors(bool jsonOutput) + { + string errorMessage = "No DDC/CI capable monitors found"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + } + + return 1; + } + + private static int HandleMonitorNotFound(List monitors, string monitorId, bool jsonOutput) + { + string errorMessage = $"Monitor '{monitorId}' not found"; + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine("Use [yellow]ddcswitch list[/] to see available monitors."); + } + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } + + private static int ExecuteToggle(Monitor monitor, uint input1Value, uint input2Value, string input1Arg, string input2Arg, bool jsonOutput) + { + // Detect current input + int detectionResult = DetectCurrentInput(monitor, jsonOutput, out uint currentInput); + bool hasWarning = false; + string? warningMessage = null; + + // Handle detection failure with fallback behavior + if (detectionResult == 2) // Warning condition - detection failed but we can continue + { + hasWarning = true; + warningMessage = "Could not detect current input source, switching to first input source"; + currentInput = 0; // Will cause DetermineTargetInput to default to input1 + } + else if (detectionResult == 1) // Hard error + { + return 1; + } + + // Determine target input + uint targetInput = DetermineTargetInput(currentInput, input1Value, input2Value); + + // Check if current input is neither of the specified inputs + if (detectionResult == 0 && currentInput != input1Value && currentInput != input2Value) + { + hasWarning = true; + warningMessage = $"Current input '{InputSource.GetName(currentInput)}' is not one of the specified inputs, switching to '{InputSource.GetName(targetInput)}'"; + } + + // Perform the input switch + return PerformInputSwitch(monitor, currentInput, targetInput, input1Arg, input2Arg, hasWarning, warningMessage, jsonOutput); + } + + private static int PerformInputSwitch(Monitor monitor, uint currentInput, uint targetInput, string input1Arg, string input2Arg, bool hasWarning, string? warningMessage, bool jsonOutput) + { + bool success = false; + string? errorMessage = null; + + if (!jsonOutput) + { + string targetName = InputSource.GetName(targetInput); + AnsiConsole.Status() + .Start($"Switching {monitor.Name} to {targetName}...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + + if (!monitor.TrySetVcpFeature(InputSource.VcpInputSource, targetInput, out int errorCode)) + { + errorMessage = GetInputSwitchErrorMessage(monitor, targetInput, errorCode); + } + else + { + success = true; + } + + if (success) + { + // Give the monitor a moment to switch inputs + Thread.Sleep(1000); + } + }); + } + else + { + if (!monitor.TrySetVcpFeature(InputSource.VcpInputSource, targetInput, out int errorCode)) + { + errorMessage = GetInputSwitchErrorMessage(monitor, targetInput, errorCode); + } + else + { + success = true; + Thread.Sleep(1000); + } + } + + if (!success) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, errorMessage!, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage!); + } + + return 1; + } + + // Output success + OutputToggleSuccess(monitor, currentInput, targetInput, hasWarning, warningMessage, jsonOutput); + return 0; + } + + private static string GetInputSwitchErrorMessage(Monitor monitor, uint targetInput, int errorCode) + { + string targetName = InputSource.GetName(targetInput); + string escapedMonitorName = monitor.Name.Replace("[", "[[").Replace("]", "]]"); + + return errorCode switch + { + 0x00000006 => $"Monitor '{escapedMonitorName}' handle is invalid - DDC/CI communication failed", + 0x00000057 => $"Monitor '{escapedMonitorName}' does not support input source switching", + 0x00000102 => $"Timeout switching monitor '{escapedMonitorName}' to {targetName} - monitor may be unresponsive", + _ => $"Failed to switch monitor '{escapedMonitorName}' to {targetName} (Error: 0x{errorCode:X8})" + }; + } + + private static void OutputToggleSuccess(Monitor monitor, uint currentInput, uint targetInput, bool hasWarning, string? warningMessage, bool jsonOutput) + { + string fromInputName = InputSource.GetName(currentInput); + string toInputName = InputSource.GetName(targetInput); + + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var result = new ToggleInputResponse( + Success: true, + Monitor: monitorRef, + FromInput: fromInputName, + ToInput: toInputName, + FromInputCode: currentInput, + ToInputCode: targetInput, + Warning: hasWarning ? warningMessage : null + ); + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.ToggleInputResponse)); + } + else + { + var successPanel = new Panel( + $"[bold cyan]Monitor:[/] {monitor.Name}\n" + + $"[bold yellow]From:[/] {fromInputName}\n" + + $"[bold green]To:[/] {toInputName}") + { + Header = new PanelHeader("[bold green]>> Input Toggled Successfully[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Green) + }; + + AnsiConsole.Write(successPanel); + + if (hasWarning && warningMessage != null) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] {warningMessage}"); + } + } + } +} \ No newline at end of file diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index 059cdf0..02a5a5c 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -7,6 +7,7 @@ namespace DDCSwitch; [JsonSerializable(typeof(MonitorInfo))] [JsonSerializable(typeof(GetVcpResponse))] [JsonSerializable(typeof(SetVcpResponse))] +[JsonSerializable(typeof(ToggleInputResponse))] [JsonSerializable(typeof(VcpScanResponse))] [JsonSerializable(typeof(VcpFeatureInfo))] [JsonSerializable(typeof(VcpFeatureType))] @@ -25,6 +26,15 @@ internal record ErrorResponse(bool Success, string Error, MonitorReference? Moni internal record ListMonitorsResponse(bool Success, List? Monitors = null, string? Error = null); internal record GetVcpResponse(bool Success, MonitorReference Monitor, string FeatureName, uint RawValue, uint MaxValue, uint? PercentageValue = null, string? ErrorMessage = null); internal record SetVcpResponse(bool Success, MonitorReference Monitor, string FeatureName, uint SetValue, uint? PercentageValue = null, string? ErrorMessage = null); +internal record ToggleInputResponse( + bool Success, + MonitorReference Monitor, + string FromInput, + string ToInput, + uint FromInputCode, + uint ToInputCode, + string? Warning = null, + string? ErrorMessage = null); internal record VcpScanResponse(bool Success, MonitorReference Monitor, List Features, string? ErrorMessage = null); // Data models diff --git a/EXAMPLES.md b/EXAMPLES.md index 1873c3d..5c4c8b8 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -261,6 +261,64 @@ ddcswitch set 0 contrast 80% ddcswitch set 0 0x10 120 # Brightness (raw value) ``` +### Toggle Between Input Sources + +The toggle command automatically switches between two specified input sources by detecting the current input and switching to the alternate one: + +```powershell +# Toggle between HDMI1 and DisplayPort1 +ddcswitch toggle 0 HDMI1 DP1 + +# Toggle between HDMI1 and HDMI2 by monitor name +ddcswitch toggle "LG ULTRAGEAR" HDMI1 HDMI2 + +# Toggle with JSON output for automation +ddcswitch toggle 0 HDMI1 DP1 --json +``` + +#### Toggle Command Behavior + +- **Current input is HDMI1** → Switches to DP1 +- **Current input is DP1** → Switches to HDMI1 +- **Current input is neither** → Switches to HDMI1 (first input) with warning + +#### Toggle Examples + +```powershell +# Create toggle shortcuts for different monitor setups +# toggle-main-monitor.ps1 +ddcswitch toggle 0 HDMI1 DP1 +Write-Host "Toggled main monitor input" -ForegroundColor Green + +# toggle-secondary-monitor.ps1 +ddcswitch toggle 1 HDMI2 DP2 +Write-Host "Toggled secondary monitor input" -ForegroundColor Green + +# Smart toggle with status feedback +$result = ddcswitch toggle 0 HDMI1 DP1 --json | ConvertFrom-Json +if ($result.success) { + Write-Host "Switched from $($result.fromInput) to $($result.toInput)" -ForegroundColor Green +} else { + Write-Host "Toggle failed: $($result.errorMessage)" -ForegroundColor Red +} +``` + +#### AutoHotkey Toggle Integration + +```autohotkey +; Ctrl+Alt+T: Toggle between HDMI1 and DisplayPort +^!t:: + Run, ddcswitch.exe toggle 0 HDMI1 DP1, , Hide + TrayTip, ddcswitch, Input toggled, 1 + return + +; Ctrl+Alt+Shift+T: Toggle secondary monitor +^!+t:: + Run, ddcswitch.exe toggle 1 HDMI2 DP2, , Hide + TrayTip, ddcswitch, Secondary monitor toggled, 1 + return +``` + ## Brightness and Contrast Control ### Basic Brightness Control diff --git a/README.md b/README.md index 67844e0..695fe61 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,25 @@ ddcswitch set 0 HDMI1 ddcswitch set "LG ULTRAGEAR" HDMI2 ``` +### Toggle Between Input Sources + +Automatically switch between two input sources without specifying which one: + +```powershell +# Toggle between HDMI1 and DisplayPort1 +ddcswitch toggle 0 HDMI1 DP1 + +# Toggle by monitor name +ddcswitch toggle "LG ULTRAGEAR" HDMI1 HDMI2 +``` + +The toggle command detects the current input and switches to the alternate one: +- If current input is HDMI1 → switches to DP1 +- If current input is DP1 → switches to HDMI1 +- If current input is neither → switches to HDMI1 (with warning) + +Perfect for hotkeys and automation where you want to switch between two specific inputs without knowing which one is currently active. + Set brightness or contrast with percentage values: ```powershell @@ -252,6 +271,15 @@ ddcswitch set 0 HDMI1 ddcswitch set 1 DP1 ``` +**Toggle between input sources:** +```powershell +# Toggle main monitor between HDMI1 and DisplayPort +ddcswitch toggle 0 HDMI1 DP1 + +# Toggle secondary monitor between HDMI inputs +ddcswitch toggle 1 HDMI1 HDMI2 +``` + **Control comprehensive VCP features:** ```powershell ddcswitch set 0 brightness 75% From 3d2c48706ac4e52a551a26adcfab3acb44236b97 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:29:23 -0600 Subject: [PATCH 02/17] Add Chocolatey installation instructions to README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 695fe61..53ed458 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,20 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display D ## Installation +### Chocolatey (Recommended) + +Install via [Chocolatey](https://chocolatey.org/) package manager: + +```powershell +choco install ddcswitch +``` + +To upgrade to the latest version: + +```powershell +choco upgrade ddcswitch +``` + ### Pre-built Binary Download the latest release from the [Releases](../../releases) page and extract `ddcswitch.exe` to a folder in your PATH. From 0ff4ae9cbfbb2442c34923997921484573c18485 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:40:24 -0600 Subject: [PATCH 03/17] Add EDID parsing functionality and update monitor data structure --- DDCSwitch/Commands/ListCommand.cs | 37 +++- DDCSwitch/DDCSwitch.csproj | 1 + DDCSwitch/EdidParser.cs | 307 ++++++++++++++++++++++++++++++ DDCSwitch/JsonContext.cs | 9 +- DDCSwitch/Monitor.cs | 36 ++++ DDCSwitch/MonitorController.cs | 4 + DDCSwitch/NativeMethods.cs | 71 ++++++- LICENSE | 2 +- 8 files changed, 461 insertions(+), 6 deletions(-) create mode 100644 DDCSwitch/EdidParser.cs diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs index 87cb204..f00371c 100644 --- a/DDCSwitch/Commands/ListCommand.cs +++ b/DDCSwitch/Commands/ListCommand.cs @@ -125,7 +125,14 @@ private static void OutputJsonList(List monitors, bool verboseOutput) inputCode != null ? $"0x{inputCode:X2}" : null, status, brightness, - contrast); + contrast, + monitor.ManufacturerId, + monitor.ManufacturerName, + monitor.ModelName, + monitor.SerialNumber, + monitor.ProductCode, + monitor.ManufactureYear, + monitor.ManufactureWeek); }).ToList(); var result = new ListMonitorsResponse(true, monitorList); @@ -142,9 +149,13 @@ private static void OutputTableList(List monitors, bool verboseOutput) .AddColumn(new TableColumn("[bold yellow]Device[/]").LeftAligned()) .AddColumn(new TableColumn("[bold yellow]Current Input[/]").LeftAligned()); - // Add brightness and contrast columns if verbose mode is enabled + // Add EDID and feature columns if verbose mode is enabled if (verboseOutput) { + table.AddColumn(new TableColumn("[bold yellow]Manufacturer[/]").LeftAligned()); + table.AddColumn(new TableColumn("[bold yellow]Model[/]").LeftAligned()); + table.AddColumn(new TableColumn("[bold yellow]Serial[/]").LeftAligned()); + table.AddColumn(new TableColumn("[bold yellow]Mfg Date[/]").LeftAligned()); table.AddColumn(new TableColumn("[bold yellow]Brightness[/]").Centered()); table.AddColumn(new TableColumn("[bold yellow]Contrast[/]").Centered()); } @@ -219,9 +230,29 @@ private static void OutputTableList(List monitors, bool verboseOutput) inputInfo }; - // Add brightness and contrast columns if verbose mode is enabled + // Add EDID and feature columns if verbose mode is enabled if (verboseOutput) { + // EDID information + string manufacturer = monitor.ManufacturerName != null + ? $"[cyan]{monitor.ManufacturerName}[/]" + : "[dim]N/A[/]"; + string model = monitor.ModelName != null + ? $"[cyan]{monitor.ModelName}[/]" + : "[dim]N/A[/]"; + string serial = monitor.SerialNumber != null + ? $"[dim]{monitor.SerialNumber}[/]" + : "[dim]N/A[/]"; + string mfgDate = monitor.ManufactureYear.HasValue + ? (monitor.ManufactureWeek.HasValue + ? $"[dim]{monitor.ManufactureYear}/W{monitor.ManufactureWeek}[/]" + : $"[dim]{monitor.ManufactureYear}[/]") + : "[dim]N/A[/]"; + + row.Add(manufacturer); + row.Add(model); + row.Add(serial); + row.Add(mfgDate); row.Add(brightnessInfo); row.Add(contrastInfo); } diff --git a/DDCSwitch/DDCSwitch.csproj b/DDCSwitch/DDCSwitch.csproj index c9d1921..835173e 100644 --- a/DDCSwitch/DDCSwitch.csproj +++ b/DDCSwitch/DDCSwitch.csproj @@ -12,6 +12,7 @@ false true ddcswitch + $(NoWarn);CA1416 diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs new file mode 100644 index 0000000..e50d6eb --- /dev/null +++ b/DDCSwitch/EdidParser.cs @@ -0,0 +1,307 @@ +using System.Text; + +namespace DDCSwitch; + +/// +/// Parses EDID (Extended Display Identification Data) blocks to extract monitor information. +/// +public static class EdidParser +{ + /// + /// Parses manufacturer ID from EDID bytes. + /// + /// EDID data (at least 2 bytes from offset 8) + /// 3-letter manufacturer ID (e.g., "SAM", "DEL") or null if invalid + public static string? ParseManufacturerId(byte[] edid) + { + if (edid.Length < 10) return null; + + try + { + // Manufacturer ID is stored in bytes 8-9 as 3 5-bit characters + // Bit layout: |0|CHAR1|CHAR2|CHAR3| across 2 bytes + ushort id = (ushort)((edid[8] << 8) | edid[9]); + + char char1 = (char)(((id >> 10) & 0x1F) + 'A' - 1); + char char2 = (char)(((id >> 5) & 0x1F) + 'A' - 1); + char char3 = (char)((id & 0x1F) + 'A' - 1); + + return $"{char1}{char2}{char3}"; + } + catch + { + return null; + } + } + + /// + /// Gets the full manufacturer name from 3-letter PNP ID. + /// + /// 3-letter manufacturer ID + /// Full company name or the ID itself if not found + public static string GetManufacturerName(string? manufacturerId) + { + if (string.IsNullOrEmpty(manufacturerId)) return "Unknown"; + + return manufacturerId switch + { + "AAC" => "AcerView", + "ACI" => "Asus Computer Inc", + "ACR" => "Acer Technologies", + "ACT" => "Targa", + "ADI" => "ADI Corporation", + "AIC" => "AG Neovo", + "AMW" => "AMW", + "AOC" => "AOC International", + "API" => "A Plus Info Corporation", + "APP" => "Apple Computer", + "ART" => "ArtMedia", + "AST" => "AST Research", + "AUO" => "AU Optronics", + "BEL" => "Belkin", + "BEN" => "BenQ Corporation", + "BMM" => "BMM", + "BNQ" => "BenQ Corporation", + "BOE" => "BOE Technology", + "CMO" => "Chi Mei Optoelectronics", + "CPL" => "Compal Electronics", + "CPQ" => "Compaq Computer Corporation", + "CTX" => "CTX International", + "DEC" => "Digital Equipment Corporation", + "DEL" => "Dell Inc.", + "DPC" => "Delta Electronics", + "DWE" => "Daewoo Electronics", + "ECS" => "ELITEGROUP Computer Systems", + "EIZ" => "EIZO Corporation", + "ELS" => "ELSA GmbH", + "ENC" => "Eizo Nanao Corporation", + "EPI" => "Envision Peripherals", + "EPH" => "Epiphan Systems Inc.", + "FUJ" => "Fujitsu Siemens Computers", + "FUS" => "Fujitsu Siemens Computers", + "GSM" => "LG Electronics", + "GWY" => "Gateway 2000", + "HEI" => "Hyundai Electronics Industries", + "HIQ" => "Hyundai ImageQuest", + "HIT" => "Hitachi", + "HPN" => "HP Inc.", + "HSD" => "Hannstar Display Corporation", + "HSL" => "Hansol Electronics", + "HTC" => "Hitachi", + "HWP" => "HP Inc.", + "IBM" => "IBM Corporation", + "ICL" => "Fujitsu ICL", + "IFS" => "InFocus Corporation", + "IQT" => "Hyundai ImageQuest", + "IVM" => "Iiyama North America", + "KDS" => "KDS USA", + "KFC" => "KFC Computek", + "LEN" => "Lenovo", + "LGD" => "LG Display", + "LKM" => "ADLAS", + "LNK" => "LINK Technologies", + "LPL" => "LG Philips", + "LTN" => "Lite-On Technology", + "MAG" => "MAG InnoVision", + "MAX" => "Maxdata Computer", + "MEI" => "Panasonic Industry Company", + "MEL" => "Mitsubishi Electronics", + "MED" => "Matsushita Electric Industrial", + "MS_" => "Panasonic Industry Company", + "MSI" => "Micro-Star International", + "MSH" => "Microsoft Corporation", + "NAN" => "NANAO Corporation", + "NEC" => "NEC Corporation", + "NOK" => "Nokia", + "NVD" => "Nvidia", + "OPT" => "Optoma Corporation", + "OQI" => "OPTIQUEST", + "PBN" => "Packard Bell", + "PCK" => "Daewoo Electronics", + "PDC" => "Polaroid", + "PGS" => "Princeton Graphic Systems", + "PHL" => "Philips Consumer Electronics", + "PIX" => "Pixelink", + "PNR" => "Planar Systems", + "PRT" => "Princeton Graphic Systems", + "REL" => "Relisys", + "SAM" => "Samsung Electric Company", + "SAN" => "Sanyo Electric Co.", + "SBI" => "Smarttech", + "SEC" => "Seiko Epson Corporation", + "SGI" => "Silicon Graphics", + "SMC" => "Samtron", + "SMI" => "Smile", + "SNI" => "Siemens Nixdorf", + "SNY" => "Sony Corporation", + "SPT" => "Sceptre Tech Inc.", + "SRC" => "Shamrock Technology", + "STN" => "Samtron", + "STP" => "Sceptre Tech Inc.", + "TAT" => "Tatung Company of America", + "TOS" => "Toshiba Corporation", + "TRL" => "Royal Information Company", + "TSB" => "Toshiba America Info Systems", + "UNK" => "Unknown", + "UNM" => "Unisys Corporation", + "VSC" => "ViewSonic Corporation", + "WTC" => "Wen Technology", + "ZCM" => "Zenith Data Systems", + _ => manufacturerId, + }; + } + + /// + /// Parses the model name from EDID descriptor blocks. + /// + /// EDID data (at least 128 bytes) + /// Model name string or null if not found + public static string? ParseModelName(byte[] edid) + { + if (edid.Length < 128) return null; + + // Check descriptor blocks at offsets 54, 72, 90, 108 (18 bytes each) + for (int offset = 54; offset <= 108; offset += 18) + { + // Descriptor type 0xFC indicates monitor name + if (edid[offset] == 0x00 && edid[offset + 1] == 0x00 && + edid[offset + 2] == 0x00 && edid[offset + 3] == 0xFC) + { + return ParseDescriptorString(edid, offset + 5); + } + } + + return null; + } + + /// + /// Parses the serial number from EDID descriptor blocks. + /// + /// EDID data (at least 128 bytes) + /// Serial number string or null if not found + public static string? ParseSerialNumber(byte[] edid) + { + if (edid.Length < 128) return null; + + // Check descriptor blocks at offsets 54, 72, 90, 108 (18 bytes each) + for (int offset = 54; offset <= 108; offset += 18) + { + // Descriptor type 0xFF indicates serial number + if (edid[offset] == 0x00 && edid[offset + 1] == 0x00 && + edid[offset + 2] == 0x00 && edid[offset + 3] == 0xFF) + { + return ParseDescriptorString(edid, offset + 5); + } + } + + return null; + } + + /// + /// Parses the product code from EDID. + /// + /// EDID data (at least 12 bytes) + /// Product code as ushort or null if invalid + public static ushort? ParseProductCode(byte[] edid) + { + if (edid.Length < 12) return null; + + try + { + // Product code is at bytes 10-11 (little-endian) + return (ushort)(edid[10] | (edid[11] << 8)); + } + catch + { + return null; + } + } + + /// + /// Parses the manufacture year from EDID. + /// + /// EDID data (at least 18 bytes) + /// Manufacture year (e.g., 2023) or null if invalid + public static int? ParseManufactureYear(byte[] edid) + { + if (edid.Length < 18) return null; + + try + { + // Manufacture year is at byte 17, stored as offset from 1990 + int year = edid[17] + 1990; + return year >= 1990 && year <= 2100 ? year : null; + } + catch + { + return null; + } + } + + /// + /// Parses the manufacture week from EDID. + /// + /// EDID data (at least 17 bytes) + /// Manufacture week (1-53) or null if invalid + public static int? ParseManufactureWeek(byte[] edid) + { + if (edid.Length < 17) return null; + + try + { + // Manufacture week is at byte 16 (1-53, or 0xFF for unknown) + int week = edid[16]; + return week >= 1 && week <= 53 ? week : null; + } + catch + { + return null; + } + } + + /// + /// Helper to parse ASCII string from EDID descriptor block. + /// + private static string? ParseDescriptorString(byte[] edid, int offset) + { + try + { + var sb = new StringBuilder(); + for (int i = 0; i < 13; i++) + { + byte b = edid[offset + i]; + if (b == 0x0A || b == 0x00) break; // Newline or null terminator + if (b >= 0x20 && b <= 0x7E) // Printable ASCII + { + sb.Append((char)b); + } + } + + string result = sb.ToString().Trim(); + return string.IsNullOrWhiteSpace(result) ? null : result; + } + catch + { + return null; + } + } + + /// + /// Validates EDID header (first 8 bytes should be: 00 FF FF FF FF FF FF 00). + /// + /// EDID data (at least 8 bytes) + /// True if header is valid + public static bool ValidateHeader(byte[] edid) + { + if (edid.Length < 8) return false; + + return edid[0] == 0x00 && + edid[1] == 0xFF && + edid[2] == 0xFF && + edid[3] == 0xFF && + edid[4] == 0xFF && + edid[5] == 0xFF && + edid[6] == 0xFF && + edid[7] == 0x00; + } +} diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index 02a5a5c..da95367 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -47,7 +47,14 @@ internal record MonitorInfo( string? CurrentInputCode, string Status, string? Brightness = null, - string? Contrast = null); + string? Contrast = null, + string? ManufacturerId = null, + string? ManufacturerName = null, + string? ModelName = null, + string? SerialNumber = null, + int? ProductCode = null, + int? ManufactureYear = null, + int? ManufactureWeek = null); internal record MonitorReference(int Index, string Name, string DeviceName, bool IsPrimary = false); diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 00dab9a..19e0e22 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -16,9 +16,45 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public string DeviceName { get; } = deviceName; public bool IsPrimary { get; } = isPrimary; + // EDID properties + public string? ManufacturerId { get; private set; } + public string? ManufacturerName { get; private set; } + public string? ModelName { get; private set; } + public string? SerialNumber { get; private set; } + public ushort? ProductCode { get; private set; } + public int? ManufactureYear { get; private set; } + public int? ManufactureWeek { get; private set; } + private IntPtr Handle { get; } = handle; private bool _disposed; + /// + /// Loads EDID data from registry and populates EDID properties. + /// + public void LoadEdidData() + { + try + { + var edid = NativeMethods.GetEdidFromRegistry(DeviceName); + if (edid == null || edid.Length < 128) return; + + ManufacturerId = EdidParser.ParseManufacturerId(edid); + if (ManufacturerId != null) + { + ManufacturerName = EdidParser.GetManufacturerName(ManufacturerId); + } + ModelName = EdidParser.ParseModelName(edid); + SerialNumber = EdidParser.ParseSerialNumber(edid); + ProductCode = EdidParser.ParseProductCode(edid); + ManufactureYear = EdidParser.ParseManufactureYear(edid); + ManufactureWeek = EdidParser.ParseManufactureWeek(edid); + } + catch + { + // Graceful degradation - EDID properties remain null + } + } + public bool TryGetInputSource(out uint currentValue, out uint maxValue) { currentValue = 0; diff --git a/DDCSwitch/MonitorController.cs b/DDCSwitch/MonitorController.cs index 0381ed4..b758021 100644 --- a/DDCSwitch/MonitorController.cs +++ b/DDCSwitch/MonitorController.cs @@ -54,6 +54,10 @@ public static List EnumerateMonitors() isPrimary, physicalMonitor.hPhysicalMonitor ); + + // Load EDID data for monitor identification + monitor.LoadEdidData(); + monitors.Add(monitor); } } diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index 5cdf881..a53aee0 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Microsoft.Win32; namespace DDCSwitch; @@ -85,5 +86,73 @@ public struct MONITORINFOEX } public const uint MONITORINFOF_PRIMARY = 0x00000001; -} + /// + /// Attempts to retrieve EDID data for a monitor from Windows Registry. + /// + /// Device name from MONITORINFOEX (e.g., \\.\DISPLAY1) + /// EDID byte array or null if not found + public static byte[]? GetEdidFromRegistry(string deviceName) + { + try + { + // Enumerate all DISPLAY devices in registry + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return null; + + // Collect all EDIDs from active monitors + var edidList = new List(); + + // Try each monitor manufacturer key + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + // Try each instance under this manufacturer + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + // Check if this is an active device + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + // Read EDID data + var edidData = deviceParams.GetValue("EDID") as byte[]; + if (edidData != null && edidData.Length >= 128) + { + // Validate EDID header + if (EdidParser.ValidateHeader(edidData)) + { + edidList.Add(edidData); + } + } + } + } + + // Extract display index from device name (e.g., \\.\DISPLAY1 -> 0-based index 0) + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (!int.TryParse(displayNum, out int displayIndex)) + return null; + + // Convert 1-based display number to 0-based index + int listIndex = displayIndex - 1; + + // Return EDID at the corresponding index if available + if (listIndex >= 0 && listIndex < edidList.Count) + { + return edidList[listIndex]; + } + + // Fallback: return first EDID if index doesn't match + return edidList.Count > 0 ? edidList[0] : null; + } + catch + { + return null; + } + } +} diff --git a/LICENSE b/LICENSE index 34771c0..2175917 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2026 ddcswitch Contributors +https://github.com/markdwags/ddcswitch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +20,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - From ff1b199a3c3aec35c6cab52e83b53fbe3b6fdb48 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:20:21 -0600 Subject: [PATCH 04/17] Add EDID information retrieval and display command for monitors --- DDCSwitch/Commands/CommandRouter.cs | 1 + DDCSwitch/Commands/ConsoleOutputFormatter.cs | 67 ++++++ DDCSwitch/Commands/HelpCommand.cs | 5 + DDCSwitch/Commands/InfoCommand.cs | 201 +++++++++++++++++ DDCSwitch/EdidParser.cs | 222 +++++++++++++++++++ DDCSwitch/JsonContext.cs | 46 ++++ DDCSwitch/Monitor.cs | 8 + EXAMPLES.md | 212 +++++++++++++++++- README.md | 25 ++- 9 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 DDCSwitch/Commands/InfoCommand.cs diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index 0cefadb..a7d03d9 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -38,6 +38,7 @@ public static int Route(string[] args) "get" => GetCommand.Execute(filteredArgs, jsonOutput), "set" => SetCommand.Execute(filteredArgs, jsonOutput), "toggle" => ToggleCommand.Execute(filteredArgs, jsonOutput), + "info" => InfoCommand.Execute(filteredArgs, jsonOutput), "version" or "-v" or "--version" => HelpCommand.ShowVersion(jsonOutput), "help" or "-h" or "--help" or "/?" => HelpCommand.ShowUsage(), _ => InvalidCommand(filteredArgs[0], jsonOutput) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 3a917f2..97dce0a 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -38,5 +38,72 @@ public static void WriteMonitorInfo(string label, string value, bool highlight = var color = highlight ? "yellow" : "cyan"; AnsiConsole.MarkupLine($" [bold {color}]{label}:[/] {value}"); } + + public static void WriteMonitorDetails(Monitor monitor) + { + WriteHeader($"Monitor {monitor.Index}: {monitor.Name}"); + + WriteMonitorInfo("Device Name", monitor.DeviceName); + WriteMonitorInfo("Is Primary", monitor.IsPrimary ? "Yes" : "No"); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [bold underline yellow]EDID Information[/]"); + + if (monitor.EdidVersion != null) + WriteMonitorInfo("EDID Version", monitor.EdidVersion.ToString()); + + if (monitor.ManufacturerId != null) + WriteMonitorInfo("Manufacturer ID", monitor.ManufacturerId); + + if (monitor.ManufacturerName != null) + WriteMonitorInfo("Manufacturer", monitor.ManufacturerName); + + if (monitor.ModelName != null) + WriteMonitorInfo("Model Name", monitor.ModelName); + + if (monitor.SerialNumber != null) + WriteMonitorInfo("Serial Number", monitor.SerialNumber); + + if (monitor.ProductCode.HasValue) + WriteMonitorInfo("Product Code", $"0x{monitor.ProductCode.Value:X4}"); + + if (monitor.ManufactureYear.HasValue) + { + var date = monitor.ManufactureWeek.HasValue + ? $"{monitor.ManufactureYear.Value} Week {monitor.ManufactureWeek.Value}" + : $"{monitor.ManufactureYear.Value}"; + WriteMonitorInfo("Manufacture Date", date); + } + + if (monitor.VideoInputDefinition != null) + { + WriteMonitorInfo("Video Input Type", monitor.VideoInputDefinition.ToString()); + } + + if (monitor.SupportedFeatures != null) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [bold underline yellow]Supported Features[/]"); + WriteMonitorInfo("Display Type", monitor.SupportedFeatures.DisplayTypeDescription); + WriteMonitorInfo("DPMS Standby", monitor.SupportedFeatures.DpmsStandby ? "Supported" : "Not supported"); + WriteMonitorInfo("DPMS Suspend", monitor.SupportedFeatures.DpmsSuspend ? "Supported" : "Not supported"); + WriteMonitorInfo("DPMS Active-Off", monitor.SupportedFeatures.DpmsActiveOff ? "Supported" : "Not supported"); + WriteMonitorInfo("Default Color Space", monitor.SupportedFeatures.DefaultColorSpace ? "Standard" : "Non-standard"); + WriteMonitorInfo("Preferred Timing Mode", monitor.SupportedFeatures.PreferredTimingMode ? "Included" : "Not included"); + WriteMonitorInfo("Continuous Frequency", monitor.SupportedFeatures.ContinuousFrequency ? "Supported" : "Not supported"); + } + + if (monitor.Chromaticity != null) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [bold underline yellow]Chromaticity Coordinates (CIE 1931)[/]"); + WriteMonitorInfo("Red", monitor.Chromaticity.Red.ToString()); + WriteMonitorInfo("Green", monitor.Chromaticity.Green.ToString()); + WriteMonitorInfo("Blue", monitor.Chromaticity.Blue.ToString()); + WriteMonitorInfo("White Point", monitor.Chromaticity.White.ToString()); + } + + AnsiConsole.WriteLine(); + } } diff --git a/DDCSwitch/Commands/HelpCommand.cs b/DDCSwitch/Commands/HelpCommand.cs index 5b2697c..f51c21d 100644 --- a/DDCSwitch/Commands/HelpCommand.cs +++ b/DDCSwitch/Commands/HelpCommand.cs @@ -61,6 +61,9 @@ public static int ShowUsage() commandsTable.AddRow( "[cyan]toggle[/] [green][/] [blue][/] [blue][/]", "Toggle between two input sources automatically"); + commandsTable.AddRow( + "[cyan]info[/] [green][/]", + "Show detailed EDID information for a specific monitor"); commandsTable.AddRow( "[cyan]version[/] [dim]or[/] [cyan]-v[/]", "Display version information"); @@ -95,6 +98,8 @@ public static int ShowUsage() AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 1 brightness 75%"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle 0 HDMI1 DP1"); AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch toggle \"Dell Monitor\" HDMI1 HDMI2"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch info 0"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch info 0 --json"); return 0; } diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs new file mode 100644 index 0000000..9720fba --- /dev/null +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -0,0 +1,201 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class InfoCommand +{ + public static int Execute(string[] args, bool jsonOutput) + { + if (args.Length < 2) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, "Monitor identifier required"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError("Monitor identifier required."); + AnsiConsole.WriteLine("Usage: ddcswitch info "); + } + + return 1; + } + + List monitors; + + if (!jsonOutput) + { + monitors = null!; + AnsiConsole.Status() + .Start("Enumerating monitors...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + monitors = MonitorController.EnumerateMonitors(); + }); + } + else + { + monitors = MonitorController.EnumerateMonitors(); + } + + if (monitors.Count == 0) + { + return HandleNoMonitors(jsonOutput); + } + + var monitor = MonitorController.FindMonitor(monitors, args[1]); + + if (monitor == null) + { + return HandleMonitorNotFound(monitors, args[1], jsonOutput); + } + + int result = DisplayMonitorInfo(monitor, jsonOutput); + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) + { + try + { + // Get current input source + string? currentInput = null; + uint? currentInputCode = null; + string status = "ok"; + + if (monitor.TryGetInputSource(out uint current, out _)) + { + currentInput = InputSource.GetName(current); + currentInputCode = current; + } + else + { + status = "no_ddc_ci"; + } + + if (jsonOutput) + { + var monitorRef = new MonitorReference( + monitor.Index, + monitor.Name, + monitor.DeviceName, + monitor.IsPrimary); + + var edidInfo = new EdidInfo( + monitor.EdidVersion?.Major, + monitor.EdidVersion?.Minor, + monitor.ManufacturerId, + monitor.ManufacturerName, + monitor.ModelName, + monitor.SerialNumber, + monitor.ProductCode.HasValue ? $"0x{monitor.ProductCode.Value:X4}" : null, + monitor.ManufactureYear, + monitor.ManufactureWeek, + monitor.VideoInputDefinition?.IsDigital, + monitor.VideoInputDefinition?.ToString(), + monitor.SupportedFeatures != null ? new FeaturesInfo( + monitor.SupportedFeatures.DisplayTypeDescription, + monitor.SupportedFeatures.DpmsStandby, + monitor.SupportedFeatures.DpmsSuspend, + monitor.SupportedFeatures.DpmsActiveOff, + monitor.SupportedFeatures.DefaultColorSpace, + monitor.SupportedFeatures.PreferredTimingMode, + monitor.SupportedFeatures.ContinuousFrequency) : null, + monitor.Chromaticity != null ? new ChromaticityInfo( + new ColorPointInfo(monitor.Chromaticity.Red.X, monitor.Chromaticity.Red.Y), + new ColorPointInfo(monitor.Chromaticity.Green.X, monitor.Chromaticity.Green.Y), + new ColorPointInfo(monitor.Chromaticity.Blue.X, monitor.Chromaticity.Blue.Y), + new ColorPointInfo(monitor.Chromaticity.White.X, monitor.Chromaticity.White.Y)) : null); + + var response = new MonitorInfoResponse( + true, + monitorRef, + status, + currentInput, + currentInputCode.HasValue ? $"0x{currentInputCode.Value:X2}" : null, + edidInfo); + + Console.WriteLine(JsonSerializer.Serialize(response, JsonContext.Default.MonitorInfoResponse)); + } + else + { + ConsoleOutputFormatter.WriteMonitorDetails(monitor); + + if (status == "ok") + { + AnsiConsole.MarkupLine(" [bold underline yellow]Current Status[/]"); + ConsoleOutputFormatter.WriteMonitorInfo("Current Input", $"{currentInput} (0x{currentInputCode:X2})"); + } + else + { + AnsiConsole.MarkupLine(" [bold yellow]Warning:[/] [yellow]DDC/CI communication not available[/]"); + } + + AnsiConsole.WriteLine(); + } + + return 0; + } + catch (Exception ex) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, $"Failed to retrieve monitor information: {ex.Message}", monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Failed to retrieve monitor information: {ex.Message}"); + } + + return 1; + } + } + + private static int HandleNoMonitors(bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, "No DDC/CI capable monitors found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError("No DDC/CI capable monitors found."); + } + + return 1; + } + + private static int HandleMonitorNotFound(List monitors, string identifier, bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, $"Monitor '{identifier}' not found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Monitor '{identifier}' not found."); + AnsiConsole.MarkupLine($"Available monitors: [cyan]{string.Join(", ", monitors.Select(m => m.Index.ToString()))}[/]"); + } + + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } +} diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs index e50d6eb..344a598 100644 --- a/DDCSwitch/EdidParser.cs +++ b/DDCSwitch/EdidParser.cs @@ -2,6 +2,68 @@ namespace DDCSwitch; +/// +/// Represents EDID version information. +/// +/// Major version number +/// Minor version number +public record EdidVersion(byte Major, byte Minor) +{ + public override string ToString() => $"{Major}.{Minor}"; +} + +/// +/// Represents video input definition from EDID. +/// +/// True if digital input, false if analog +/// Raw byte value from EDID +public record VideoInputDefinition(bool IsDigital, byte RawValue) +{ + public override string ToString() => IsDigital ? "Digital" : "Analog"; +} + +/// +/// Represents supported display features from EDID. +/// +public record SupportedFeatures( + bool DpmsStandby, + bool DpmsSuspend, + bool DpmsActiveOff, + byte DisplayType, + bool DefaultColorSpace, + bool PreferredTimingMode, + bool ContinuousFrequency, + byte RawValue) +{ + public string DisplayTypeDescription => DisplayType switch + { + 0 => "Monochrome or Grayscale", + 1 => "RGB Color", + 2 => "Non-RGB Color", + 3 => "Undefined", + _ => "Unknown" + }; +} + +/// +/// Represents chromaticity coordinates for a color point. +/// +/// X coordinate (0.0 to 1.0) +/// Y coordinate (0.0 to 1.0) +public record ColorPoint(double X, double Y) +{ + public override string ToString() => $"x={X:F4}, y={Y:F4}"; +} + +/// +/// Represents complete chromaticity information from EDID. +/// +public record ChromaticityCoordinates( + ColorPoint Red, + ColorPoint Green, + ColorPoint Blue, + ColorPoint White); + /// /// Parses EDID (Extended Display Identification Data) blocks to extract monitor information. /// @@ -194,9 +256,37 @@ public static string GetManufacturerName(string? manufacturerId) } } + // If no descriptor serial found, try numeric serial at bytes 12-15 + var numericSerial = ParseNumericSerialNumber(edid); + if (numericSerial.HasValue && numericSerial.Value != 0) + { + return numericSerial.Value.ToString(); + } + return null; } + /// + /// Parses the numeric serial number from EDID. + /// + /// EDID data (at least 16 bytes) + /// Numeric serial number or null if invalid + public static uint? ParseNumericSerialNumber(byte[] edid) + { + if (edid.Length < 16) return null; + + try + { + // Serial number is at bytes 12-15 (little-endian, 32-bit) + uint serial = (uint)(edid[12] | (edid[13] << 8) | (edid[14] << 16) | (edid[15] << 24)); + return serial; + } + catch + { + return null; + } + } + /// /// Parses the product code from EDID. /// @@ -304,4 +394,136 @@ public static bool ValidateHeader(byte[] edid) edid[6] == 0xFF && edid[7] == 0x00; } + + /// + /// Parses EDID version and revision from EDID. + /// + /// EDID data (at least 20 bytes) + /// EDID version information or null if invalid + public static EdidVersion? ParseEdidVersion(byte[] edid) + { + if (edid.Length < 20) return null; + + try + { + // EDID version is at bytes 18-19 + byte major = edid[18]; + byte minor = edid[19]; + return new EdidVersion(major, minor); + } + catch + { + return null; + } + } + + /// + /// Parses video input definition from EDID. + /// + /// EDID data (at least 21 bytes) + /// Video input definition or null if invalid + public static VideoInputDefinition? ParseVideoInputDefinition(byte[] edid) + { + if (edid.Length < 21) return null; + + try + { + // Video input definition is at byte 20 + byte value = edid[20]; + bool isDigital = (value & 0x80) != 0; // Bit 7 + return new VideoInputDefinition(isDigital, value); + } + catch + { + return null; + } + } + + /// + /// Parses supported features from EDID. + /// + /// EDID data (at least 25 bytes) + /// Supported features information or null if invalid + public static SupportedFeatures? ParseSupportedFeatures(byte[] edid) + { + if (edid.Length < 25) return null; + + try + { + // Supported features is at byte 24 + byte value = edid[24]; + + bool dpmsStandby = (value & 0x80) != 0; // Bit 7 + bool dpmsSuspend = (value & 0x40) != 0; // Bit 6 + bool dpmsActiveOff = (value & 0x20) != 0; // Bit 5 + byte displayType = (byte)((value >> 3) & 0x03); // Bits 4-3 + bool defaultColorSpace = (value & 0x04) != 0; // Bit 2 + bool preferredTimingMode = (value & 0x02) != 0; // Bit 1 + bool continuousFrequency = (value & 0x01) != 0; // Bit 0 + + return new SupportedFeatures( + dpmsStandby, + dpmsSuspend, + dpmsActiveOff, + displayType, + defaultColorSpace, + preferredTimingMode, + continuousFrequency, + value); + } + catch + { + return null; + } + } + + /// + /// Parses chromaticity coordinates (color points for red, green, blue, and white) from EDID. + /// + /// EDID data (at least 35 bytes) + /// Chromaticity coordinates or null if invalid + public static ChromaticityCoordinates? ParseChromaticity(byte[] edid) + { + if (edid.Length < 35) return null; + + try + { + // Chromaticity data is stored in bytes 25-34 + // Each coordinate is a 10-bit value split between two bytes + byte lsb = edid[25]; // Low-order bits for red/green X + byte lsb2 = edid[26]; // Low-order bits for red/green Y + + // Red X: bits 7-6 of byte 27 (MSB) + all 8 bits of byte 25 bits 1-0 (LSB) + // Red Y: bits 5-4 of byte 27 (MSB) + all 8 bits of byte 26 bits 1-0 (LSB) + int redXRaw = ((edid[27] & 0xC0) << 2) | ((lsb >> 6) & 0x03); + int redYRaw = ((edid[27] & 0x30) << 4) | ((lsb2 >> 6) & 0x03); + + // Green X: bits 7-6 of byte 28 + byte 25 bits 5-4 + // Green Y: bits 5-4 of byte 28 + byte 26 bits 5-4 + int greenXRaw = ((edid[28] & 0xC0) << 2) | ((lsb >> 4) & 0x03); + int greenYRaw = ((edid[28] & 0x30) << 4) | ((lsb2 >> 4) & 0x03); + + // Blue X: bits 7-6 of byte 29 + byte 25 bits 3-2 + // Blue Y: bits 5-4 of byte 29 + byte 26 bits 3-2 + int blueXRaw = ((edid[29] & 0xC0) << 2) | ((lsb >> 2) & 0x03); + int blueYRaw = ((edid[29] & 0x30) << 4) | ((lsb2 >> 2) & 0x03); + + // White X: bits 7-6 of byte 30 + byte 25 bits 1-0 + // White Y: bits 5-4 of byte 30 + byte 26 bits 1-0 + int whiteXRaw = ((edid[30] & 0xC0) << 2) | (lsb & 0x03); + int whiteYRaw = ((edid[30] & 0x30) << 4) | (lsb2 & 0x03); + + // Convert 10-bit values to 0.0-1.0 range + var red = new ColorPoint(redXRaw / 1024.0, redYRaw / 1024.0); + var green = new ColorPoint(greenXRaw / 1024.0, greenYRaw / 1024.0); + var blue = new ColorPoint(blueXRaw / 1024.0, blueYRaw / 1024.0); + var white = new ColorPoint(whiteXRaw / 1024.0, whiteYRaw / 1024.0); + + return new ChromaticityCoordinates(red, green, blue, white); + } + catch + { + return null; + } + } } diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index da95367..9ea4b3b 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -12,6 +12,11 @@ namespace DDCSwitch; [JsonSerializable(typeof(VcpFeatureInfo))] [JsonSerializable(typeof(VcpFeatureType))] [JsonSerializable(typeof(MonitorReference))] +[JsonSerializable(typeof(MonitorInfoResponse))] +[JsonSerializable(typeof(EdidInfo))] +[JsonSerializable(typeof(FeaturesInfo))] +[JsonSerializable(typeof(ChromaticityInfo))] +[JsonSerializable(typeof(ColorPointInfo))] [JsonSourceGenerationOptions( WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, @@ -58,3 +63,44 @@ internal record MonitorInfo( internal record MonitorReference(int Index, string Name, string DeviceName, bool IsPrimary = false); +internal record MonitorInfoResponse( + bool Success, + MonitorReference Monitor, + string Status, + string? CurrentInput = null, + string? CurrentInputCode = null, + EdidInfo? Edid = null, + string? ErrorMessage = null); + +internal record EdidInfo( + byte? VersionMajor, + byte? VersionMinor, + string? ManufacturerId, + string? ManufacturerName, + string? ModelName, + string? SerialNumber, + string? ProductCode, + int? ManufactureYear, + int? ManufactureWeek, + bool? IsDigitalInput, + string? VideoInputType, + FeaturesInfo? Features, + ChromaticityInfo? Chromaticity); + +internal record FeaturesInfo( + string DisplayType, + bool DpmsStandby, + bool DpmsSuspend, + bool DpmsActiveOff, + bool DefaultColorSpace, + bool PreferredTimingMode, + bool ContinuousFrequency); + +internal record ChromaticityInfo( + ColorPointInfo Red, + ColorPointInfo Green, + ColorPointInfo Blue, + ColorPointInfo White); + +internal record ColorPointInfo(double X, double Y); + diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 19e0e22..5e9a6e6 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -24,6 +24,10 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public ushort? ProductCode { get; private set; } public int? ManufactureYear { get; private set; } public int? ManufactureWeek { get; private set; } + public EdidVersion? EdidVersion { get; private set; } + public VideoInputDefinition? VideoInputDefinition { get; private set; } + public SupportedFeatures? SupportedFeatures { get; private set; } + public ChromaticityCoordinates? Chromaticity { get; private set; } private IntPtr Handle { get; } = handle; private bool _disposed; @@ -48,6 +52,10 @@ public void LoadEdidData() ProductCode = EdidParser.ParseProductCode(edid); ManufactureYear = EdidParser.ParseManufactureYear(edid); ManufactureWeek = EdidParser.ParseManufactureWeek(edid); + EdidVersion = EdidParser.ParseEdidVersion(edid); + VideoInputDefinition = EdidParser.ParseVideoInputDefinition(edid); + SupportedFeatures = EdidParser.ParseSupportedFeatures(edid); + Chromaticity = EdidParser.ParseChromaticity(edid); } catch { diff --git a/EXAMPLES.md b/EXAMPLES.md index 5c4c8b8..b90b2d6 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,6 +1,216 @@ # ddcswitch Examples -This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, and automation. +This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, EDID information retrieval, and automation. + +## Monitor Information (EDID) + +Retrieve detailed Extended Display Identification Data (EDID) from your monitors to view specifications, capabilities, and color characteristics. + +### Basic EDID Information + +View all EDID data for a specific monitor: + +```powershell +ddcswitch info 0 +``` + +Example output: +``` +── Monitor 0: Generic PnP Monitor ─────────────────────────────── + Device Name: \\.\DISPLAY2 + Is Primary: No + + EDID Information + EDID Version: 1.4 + Manufacturer ID: ACR + Manufacturer: Acer Technologies + Model Name: VG270U P + Product Code: 0x06CF + Manufacture Date: 2021 Week 2 + Video Input Type: Digital + + Supported Features + Display Type: RGB Color + DPMS Standby: Supported + DPMS Suspend: Not supported + DPMS Active-Off: Supported + Default Color Space: Standard + Preferred Timing Mode: Included + Continuous Frequency: Supported + + Chromaticity Coordinates (CIE 1931) + Red: x=0.6396, y=0.3300 + Green: x=0.2998, y=0.5996 + Blue: x=0.1503, y=0.0595 + White Point: x=0.3125, y=0.3291 + + Current Status + Current Input: HDMI1 (0x11) +``` + +### JSON Output for EDID Data + +Retrieve EDID data in JSON format for programmatic access: + +```powershell +ddcswitch info 0 --json +``` + +Example JSON output: +```json +{ + "success": true, + "monitor": { + "index": 0, + "name": "Generic PnP Monitor", + "deviceName": "\\\\.\\DISPLAY2", + "isPrimary": false + }, + "status": "ok", + "currentInput": "HDMI1", + "currentInputCode": "0x11", + "edid": { + "versionMajor": 1, + "versionMinor": 4, + "manufacturerId": "ACR", + "manufacturerName": "Acer Technologies", + "modelName": "VG270U P", + "productCode": "0x06CF", + "manufactureYear": 2021, + "manufactureWeek": 2, + "isDigitalInput": true, + "videoInputType": "Digital", + "features": { + "displayType": "RGB Color", + "dpmsStandby": false, + "dpmsSuspend": false, + "dpmsActiveOff": true, + "defaultColorSpace": false, + "preferredTimingMode": true, + "continuousFrequency": true + }, + "chromaticity": { + "red": { "x": 0.6396484375, "y": 0.330078125 }, + "green": { "x": 0.2998046875, "y": 0.599609375 }, + "blue": { "x": 0.150390625, "y": 0.0595703125 }, + "white": { "x": 0.3125, "y": 0.32910156 } + } + } +} +``` + +### Automation Examples with EDID Data + +#### PowerShell: Check Monitor Model Before Applying Settings + +```powershell +# Get EDID info and apply settings only to specific model +$info = ddcswitch info 0 --json | ConvertFrom-Json + +if ($info.edid.modelName -like "*VG270U*") { + Write-Host "Configuring Acer VG270U..." -ForegroundColor Green + ddcswitch set 0 brightness 80% + ddcswitch set 0 contrast 75% + ddcswitch set 0 input HDMI1 +} +else { + Write-Host "Monitor model: $($info.edid.modelName)" -ForegroundColor Yellow +} +``` + +#### PowerShell: Log Monitor Information + +```powershell +# Create monitor inventory with EDID details +$monitors = @() +$listOutput = ddcswitch list --json | ConvertFrom-Json + +foreach ($monitor in $listOutput.monitors) { + $edidInfo = ddcswitch info $monitor.index --json | ConvertFrom-Json + + $monitors += [PSCustomObject]@{ + Index = $monitor.index + Name = $monitor.name + Manufacturer = $edidInfo.edid.manufacturerName + Model = $edidInfo.edid.modelName + Serial = $edidInfo.edid.serialNumber + EdidVersion = "$($edidInfo.edid.versionMajor).$($edidInfo.edid.versionMinor)" + VideoInput = $edidInfo.edid.videoInputType + ManufactureDate = "$($edidInfo.edid.manufactureYear) Week $($edidInfo.edid.manufactureWeek)" + CurrentInput = $edidInfo.currentInput + } +} + +$monitors | Format-Table -AutoSize +``` + +#### Python: Color Calibration Using Chromaticity Data + +```python +import subprocess +import json + +def get_monitor_chromaticity(monitor_index): + """Get chromaticity coordinates for color calibration.""" + result = subprocess.run( + ['ddcswitch', 'info', str(monitor_index), '--json'], + capture_output=True, + text=True + ) + + data = json.loads(result.stdout) + if data['success'] and 'chromaticity' in data['edid']: + chroma = data['edid']['chromaticity'] + return { + 'red': (chroma['red']['x'], chroma['red']['y']), + 'green': (chroma['green']['x'], chroma['green']['y']), + 'blue': (chroma['blue']['x'], chroma['blue']['y']), + 'white': (chroma['white']['x'], chroma['white']['y']) + } + return None + +# Use chromaticity data for color management +chroma = get_monitor_chromaticity(0) +if chroma: + print(f"Monitor Color Gamut:") + print(f" Red: x={chroma['red'][0]:.4f}, y={chroma['red'][1]:.4f}") + print(f" Green: x={chroma['green'][0]:.4f}, y={chroma['green'][1]:.4f}") + print(f" Blue: x={chroma['blue'][0]:.4f}, y={chroma['blue'][1]:.4f}") + print(f" White: x={chroma['white'][0]:.4f}, y={chroma['white'][1]:.4f}") +``` + +#### Node.js: Monitor Fleet Management + +```javascript +const { execSync } = require('child_process'); + +function getMonitorInfo(index) { + const output = execSync(`ddcswitch info ${index} --json`, { encoding: 'utf8' }); + return JSON.parse(output); +} + +// Create monitor inventory +function inventoryMonitors() { + const list = JSON.parse(execSync('ddcswitch list --json', { encoding: 'utf8' })); + + const inventory = list.monitors.map(monitor => { + const info = getMonitorInfo(monitor.index); + return { + index: monitor.index, + manufacturer: info.edid?.manufacturerName || 'Unknown', + model: info.edid?.modelName || 'Unknown', + edidVersion: `${info.edid?.versionMajor}.${info.edid?.versionMinor}`, + isDigital: info.edid?.isDigitalInput, + manufactureYear: info.edid?.manufactureYear, + currentInput: info.currentInput + }; + }); + + console.table(inventory); +} + +inventoryMonitors(); +``` ## Comprehensive VCP Feature Examples diff --git a/README.md b/README.md index 53ed458..547221c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display D ## Features - 🖥️ **List all DDC/CI capable monitors** with their current input sources -- 🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) +- � **Detailed EDID information** - View monitor specifications, capabilities, and color characteristics +- �🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) - 🔆 **Control brightness and contrast** with percentage values (0-100%) - 🎛️ **Comprehensive VCP feature support** - Access all MCCS standardized monitor controls - 🏷️ **Feature categories and discovery** - Browse VCP features by category (Image, Color, Geometry, Audio, etc.) @@ -103,6 +104,28 @@ Example output: Add `--json` for machine-readable output (see [EXAMPLES.md](EXAMPLES.md) for automation examples). +### Monitor Information (EDID) + +View detailed EDID (Extended Display Identification Data) information for a specific monitor: + +```powershell +ddcswitch info 0 +``` + +The info command provides comprehensive monitor details including: +- **EDID version** and manufacturer information +- **Model name**, serial number, and manufacture date +- **Video input type** (Digital/Analog) +- **Supported features** (DPMS power modes, display type, color space) +- **Chromaticity coordinates** for color calibration (red, green, blue, white points in CIE 1931 color space) +- **Current input source** status + +JSON output is supported with `--json` flag for programmatic access to all EDID data: + +```powershell +ddcswitch info 0 --json +``` + ### Get Current Settings Get all VCP features for a specific monitor: From 06e264fb7dbe4a82e20eb783bef7e4366219585b Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:22:03 -0600 Subject: [PATCH 05/17] Add info command to display detailed EDID information with JSON output support --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb9193..a2904b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to ddcswitch will be documented in this file. ### Added - Added the toggle command to flip a monitor between two input sources +- `info` command to display detailed EDID (Extended Display Identification Data) information + - Full JSON output support for programmatic access ## [1.0.2] - 2026-01-07 From f7a2556030e2ce0ed3a7b7734e2fd82e524d9578 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:26:38 -0600 Subject: [PATCH 06/17] Refactor manufacture date display to include month name and week number --- DDCSwitch/Commands/ConsoleOutputFormatter.cs | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 97dce0a..5b44563 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -70,7 +70,7 @@ public static void WriteMonitorDetails(Monitor monitor) if (monitor.ManufactureYear.HasValue) { var date = monitor.ManufactureWeek.HasValue - ? $"{monitor.ManufactureYear.Value} Week {monitor.ManufactureWeek.Value}" + ? FormatManufactureDate(monitor.ManufactureYear.Value, monitor.ManufactureWeek.Value) : $"{monitor.ManufactureYear.Value}"; WriteMonitorInfo("Manufacture Date", date); } @@ -105,5 +105,34 @@ public static void WriteMonitorDetails(Monitor monitor) AnsiConsole.WriteLine(); } + + private static string FormatManufactureDate(int year, int week) + { + // Calculate approximate month from week number + // Week 1-4: January, Week 5-8: February, etc. + string? monthName = week switch + { + >= 1 and <= 4 => "January", + >= 5 and <= 8 => "February", + >= 9 and <= 13 => "March", + >= 14 and <= 17 => "April", + >= 18 and <= 22 => "May", + >= 23 and <= 26 => "June", + >= 27 and <= 30 => "July", + >= 31 and <= 35 => "August", + >= 36 and <= 39 => "September", + >= 40 and <= 43 => "October", + >= 44 and <= 48 => "November", + >= 49 and <= 53 => "December", + _ => null + }; + + if (monthName != null) + { + return $"{monthName} {year} (Week {week})"; + } + + return $"{year} Week {week}"; + } } From 4454b6f9416d7aa612f0d0a15caf9368f353dda8 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:29:23 -0600 Subject: [PATCH 07/17] Enhance monitor details display with structured panels and tables for better readability --- DDCSwitch/Commands/ConsoleOutputFormatter.cs | 154 +++++++++++++++---- DDCSwitch/Commands/InfoCommand.cs | 22 ++- 2 files changed, 139 insertions(+), 37 deletions(-) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 5b44563..97d6e2d 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -41,69 +41,155 @@ public static void WriteMonitorInfo(string label, string value, bool highlight = public static void WriteMonitorDetails(Monitor monitor) { - WriteHeader($"Monitor {monitor.Index}: {monitor.Name}"); - - WriteMonitorInfo("Device Name", monitor.DeviceName); - WriteMonitorInfo("Is Primary", monitor.IsPrimary ? "Yes" : "No"); - + // Header with monitor identification + var headerPanel = new Panel( + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [cyan]{monitor.DeviceName}[/] " + + $"[dim]Primary:[/] {(monitor.IsPrimary ? "[green]Yes[/]" : "[dim]No[/]")}") + { + Header = new PanelHeader($"[bold cyan]Monitor {monitor.Index}[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Cyan1) + }; + AnsiConsole.Write(headerPanel); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" [bold underline yellow]EDID Information[/]"); - + + // EDID Information in a table + var edidTable = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Grey) + .AddColumn(new TableColumn("[bold yellow]Property[/]").Width(20)) + .AddColumn(new TableColumn("[bold yellow]Value[/]")); + if (monitor.EdidVersion != null) - WriteMonitorInfo("EDID Version", monitor.EdidVersion.ToString()); - - if (monitor.ManufacturerId != null) - WriteMonitorInfo("Manufacturer ID", monitor.ManufacturerId); + edidTable.AddRow("[cyan]EDID Version[/]", $"[white]{monitor.EdidVersion}[/]"); if (monitor.ManufacturerName != null) - WriteMonitorInfo("Manufacturer", monitor.ManufacturerName); + edidTable.AddRow("[cyan]Manufacturer[/]", $"[white]{monitor.ManufacturerName}[/] [dim]({monitor.ManufacturerId})[/]"); + else if (monitor.ManufacturerId != null) + edidTable.AddRow("[cyan]Manufacturer ID[/]", $"[white]{monitor.ManufacturerId}[/]"); if (monitor.ModelName != null) - WriteMonitorInfo("Model Name", monitor.ModelName); + edidTable.AddRow("[cyan]Model Name[/]", $"[white]{monitor.ModelName}[/]"); if (monitor.SerialNumber != null) - WriteMonitorInfo("Serial Number", monitor.SerialNumber); + edidTable.AddRow("[cyan]Serial Number[/]", $"[white]{monitor.SerialNumber}[/]"); if (monitor.ProductCode.HasValue) - WriteMonitorInfo("Product Code", $"0x{monitor.ProductCode.Value:X4}"); + edidTable.AddRow("[cyan]Product Code[/]", $"[white]0x{monitor.ProductCode.Value:X4}[/]"); if (monitor.ManufactureYear.HasValue) { var date = monitor.ManufactureWeek.HasValue ? FormatManufactureDate(monitor.ManufactureYear.Value, monitor.ManufactureWeek.Value) : $"{monitor.ManufactureYear.Value}"; - WriteMonitorInfo("Manufacture Date", date); + edidTable.AddRow("[cyan]Manufacture Date[/]", $"[white]{date}[/]"); } if (monitor.VideoInputDefinition != null) { - WriteMonitorInfo("Video Input Type", monitor.VideoInputDefinition.ToString()); + var inputColor = monitor.VideoInputDefinition.IsDigital ? "green" : "yellow"; + edidTable.AddRow("[cyan]Video Input[/]", $"[{inputColor}]{monitor.VideoInputDefinition}[/]"); } - + + var edidPanel = new Panel(edidTable) + { + Header = new PanelHeader("[bold yellow]📋 EDID Information[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(edidPanel); + AnsiConsole.WriteLine(); + + // Supported Features if (monitor.SupportedFeatures != null) { + var featuresGrid = new Grid(); + featuresGrid.AddColumn(new GridColumn().Width(25)); + featuresGrid.AddColumn(new GridColumn()); + + featuresGrid.AddRow( + "[cyan]Display Type:[/]", + $"[white]{monitor.SupportedFeatures.DisplayTypeDescription}[/]"); + + featuresGrid.AddRow( + "[cyan]DPMS Standby:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.DpmsStandby)); + + featuresGrid.AddRow( + "[cyan]DPMS Suspend:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.DpmsSuspend)); + + featuresGrid.AddRow( + "[cyan]DPMS Active-Off:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.DpmsActiveOff)); + + featuresGrid.AddRow( + "[cyan]Default Color Space:[/]", + monitor.SupportedFeatures.DefaultColorSpace ? "[green]Standard[/]" : "[dim]Non-standard[/]"); + + featuresGrid.AddRow( + "[cyan]Preferred Timing:[/]", + monitor.SupportedFeatures.PreferredTimingMode ? "[green]✓ Included[/]" : "[dim]Not included[/]"); + + featuresGrid.AddRow( + "[cyan]Continuous Frequency:[/]", + FormatFeatureSupport(monitor.SupportedFeatures.ContinuousFrequency)); + + var featuresPanel = new Panel(featuresGrid) + { + Header = new PanelHeader("[bold magenta]⚡ Supported Features[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Magenta) + }; + AnsiConsole.Write(featuresPanel); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" [bold underline yellow]Supported Features[/]"); - WriteMonitorInfo("Display Type", monitor.SupportedFeatures.DisplayTypeDescription); - WriteMonitorInfo("DPMS Standby", monitor.SupportedFeatures.DpmsStandby ? "Supported" : "Not supported"); - WriteMonitorInfo("DPMS Suspend", monitor.SupportedFeatures.DpmsSuspend ? "Supported" : "Not supported"); - WriteMonitorInfo("DPMS Active-Off", monitor.SupportedFeatures.DpmsActiveOff ? "Supported" : "Not supported"); - WriteMonitorInfo("Default Color Space", monitor.SupportedFeatures.DefaultColorSpace ? "Standard" : "Non-standard"); - WriteMonitorInfo("Preferred Timing Mode", monitor.SupportedFeatures.PreferredTimingMode ? "Included" : "Not included"); - WriteMonitorInfo("Continuous Frequency", monitor.SupportedFeatures.ContinuousFrequency ? "Supported" : "Not supported"); } - + + // Chromaticity Coordinates if (monitor.Chromaticity != null) { + var chromaTable = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.Grey) + .AddColumn(new TableColumn("[bold]Color[/]").Centered().Width(12)) + .AddColumn(new TableColumn("[bold]X[/]").Centered().Width(12)) + .AddColumn(new TableColumn("[bold]Y[/]").Centered().Width(12)); + + chromaTable.AddRow( + "[red]● Red[/]", + $"[white]{monitor.Chromaticity.Red.X:F4}[/]", + $"[white]{monitor.Chromaticity.Red.Y:F4}[/]"); + + chromaTable.AddRow( + "[green]● Green[/]", + $"[white]{monitor.Chromaticity.Green.X:F4}[/]", + $"[white]{monitor.Chromaticity.Green.Y:F4}[/]"); + + chromaTable.AddRow( + "[blue]● Blue[/]", + $"[white]{monitor.Chromaticity.Blue.X:F4}[/]", + $"[white]{monitor.Chromaticity.Blue.Y:F4}[/]"); + + chromaTable.AddRow( + "[grey]● White[/]", + $"[white]{monitor.Chromaticity.White.X:F4}[/]", + $"[white]{monitor.Chromaticity.White.Y:F4}[/]"); + + var chromaPanel = new Panel(chromaTable) + { + Header = new PanelHeader("[bold blue]🎨 Chromaticity Coordinates (CIE 1931)[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Blue) + }; + AnsiConsole.Write(chromaPanel); AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(" [bold underline yellow]Chromaticity Coordinates (CIE 1931)[/]"); - WriteMonitorInfo("Red", monitor.Chromaticity.Red.ToString()); - WriteMonitorInfo("Green", monitor.Chromaticity.Green.ToString()); - WriteMonitorInfo("Blue", monitor.Chromaticity.Blue.ToString()); - WriteMonitorInfo("White Point", monitor.Chromaticity.White.ToString()); } - - AnsiConsole.WriteLine(); + } + + private static string FormatFeatureSupport(bool supported) + { + return supported ? "[green]✓ Supported[/]" : "[dim]✗ Not supported[/]"; } private static string FormatManufactureDate(int year, int week) diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs index 9720fba..36e431e 100644 --- a/DDCSwitch/Commands/InfoCommand.cs +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -131,14 +131,30 @@ private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) { ConsoleOutputFormatter.WriteMonitorDetails(monitor); + // Current Status Panel if (status == "ok") { - AnsiConsole.MarkupLine(" [bold underline yellow]Current Status[/]"); - ConsoleOutputFormatter.WriteMonitorInfo("Current Input", $"{currentInput} (0x{currentInputCode:X2})"); + var statusPanel = new Panel( + $"[green]✓ DDC/CI Active[/]\n" + + $"[cyan]Current Input:[/] [white]{currentInput}[/] [dim](0x{currentInputCode:X2})[/]") + { + Header = new PanelHeader("[bold green]📡 Current Status[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Green) + }; + AnsiConsole.Write(statusPanel); } else { - AnsiConsole.MarkupLine(" [bold yellow]Warning:[/] [yellow]DDC/CI communication not available[/]"); + var warningPanel = new Panel( + "[yellow]DDC/CI communication not available[/]\n" + + "[dim]Input source switching may not be supported on this monitor[/]") + { + Header = new PanelHeader("[bold yellow]⚠ Warning[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(warningPanel); } AnsiConsole.WriteLine(); From 78866126e97a852279b340ba56a15459b8c46f6f Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:45:38 -0600 Subject: [PATCH 08/17] Improve error handling and performance in command processing and feature mapping --- DDCSwitch/Commands/CommandRouter.cs | 27 ++++++++++++++++++++++++++- DDCSwitch/Commands/VcpScanCommand.cs | 13 +++++++++---- DDCSwitch/FeatureResolver.cs | 23 +++++++++++++++-------- DDCSwitch/NativeMethods.cs | 18 +++++++++++++++++- DDCSwitch/VcpFeature.Generated.cs | 4 +++- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs index a7d03d9..9c46922 100644 --- a/DDCSwitch/Commands/CommandRouter.cs +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -44,6 +44,32 @@ public static int Route(string[] args) _ => InvalidCommand(filteredArgs[0], jsonOutput) }; } + catch (ArgumentException ex) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, ex.Message); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(ex.Message); + } + return 1; + } + catch (InvalidOperationException ex) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, ex.Message); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(ex.Message); + } + return 1; + } catch (Exception ex) { if (jsonOutput) @@ -55,7 +81,6 @@ public static int Route(string[] args) { ConsoleOutputFormatter.WriteError(ex.Message); } - return 1; } } diff --git a/DDCSwitch/Commands/VcpScanCommand.cs b/DDCSwitch/Commands/VcpScanCommand.cs index 971465e..b2466ae 100644 --- a/DDCSwitch/Commands/VcpScanCommand.cs +++ b/DDCSwitch/Commands/VcpScanCommand.cs @@ -108,10 +108,15 @@ public static int ScanSingleMonitor(string monitorIdentifier, bool jsonOutput) } // Filter only supported features for cleaner output - var supportedFeatures = features.Values - .Where(f => f.IsSupported) - .OrderBy(f => f.Code) - .ToList(); + var supportedFeatures = new List(features.Count); + foreach (var feature in features.Values) + { + if (feature.IsSupported) + { + supportedFeatures.Add(feature); + } + } + supportedFeatures.Sort((a, b) => a.Code.CompareTo(b.Code)); if (jsonOutput) { diff --git a/DDCSwitch/FeatureResolver.cs b/DDCSwitch/FeatureResolver.cs index ffb3f51..26c6af4 100644 --- a/DDCSwitch/FeatureResolver.cs +++ b/DDCSwitch/FeatureResolver.cs @@ -15,7 +15,7 @@ public static class FeatureResolver /// private static Dictionary BuildFeatureMap() { - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + var map = new Dictionary(capacity: 200, StringComparer.OrdinalIgnoreCase); foreach (var feature in VcpFeature.AllFeatures) { @@ -37,7 +37,7 @@ private static Dictionary BuildFeatureMap() /// private static Dictionary BuildCodeMap() { - var map = new Dictionary(); + var map = new Dictionary(capacity: 150); foreach (var feature in VcpFeature.AllFeatures) { @@ -202,10 +202,11 @@ public static bool TryParseVcpCode(string input, out byte vcpCode) input = input.Trim(); // Try hex format (0x10, 0X10) - if (input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || - input.StartsWith("0X", StringComparison.OrdinalIgnoreCase)) + ReadOnlySpan inputSpan = input.AsSpan(); + if (inputSpan.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || + inputSpan.StartsWith("0X", StringComparison.OrdinalIgnoreCase)) { - var hexPart = input.Substring(2); + ReadOnlySpan hexPart = inputSpan.Slice(2); if (byte.TryParse(hexPart, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out vcpCode)) { // VCP codes are inherently valid for byte range (0x00-0xFF) @@ -246,12 +247,18 @@ public static bool TryParsePercentage(string input, out uint percentage) input = input.Trim(); // Remove % suffix if present - if (input.EndsWith("%")) + ReadOnlySpan inputSpan = input.AsSpan(); + if (inputSpan.EndsWith("%")) { - input = input.Substring(0, input.Length - 1).Trim(); + inputSpan = inputSpan.Slice(0, inputSpan.Length - 1); + // Trim trailing whitespace after removing % + while (inputSpan.Length > 0 && char.IsWhiteSpace(inputSpan[inputSpan.Length - 1])) + { + inputSpan = inputSpan.Slice(0, inputSpan.Length - 1); + } } - if (!uint.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out percentage)) + if (!uint.TryParse(inputSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out percentage)) { return false; } diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index a53aee0..bd3810b 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -150,8 +150,24 @@ public struct MONITORINFOEX // Fallback: return first EDID if index doesn't match return edidList.Count > 0 ? edidList[0] : null; } - catch + catch (System.Security.SecurityException) { + // No access to registry + return null; + } + catch (System.UnauthorizedAccessException) + { + // Access denied + return null; + } + catch (System.IO.IOException) + { + // Registry I/O error + return null; + } + catch (Exception) + { + // Unexpected error return null; } } diff --git a/DDCSwitch/VcpFeature.Generated.cs b/DDCSwitch/VcpFeature.Generated.cs index 554118c..c4a2723 100644 --- a/DDCSwitch/VcpFeature.Generated.cs +++ b/DDCSwitch/VcpFeature.Generated.cs @@ -779,7 +779,7 @@ public partial class VcpFeature /// /// All features registry - contains all predefined MCCS features /// - public static IReadOnlyList AllFeatures { get; } = new List + private static readonly VcpFeature[] _allFeatures = new VcpFeature[] { Degauss, NewControlValue, @@ -932,4 +932,6 @@ public partial class VcpFeature ScratchPad, VcpVersion }; + + public static IReadOnlyList AllFeatures => _allFeatures; } From fe10fa56b1d5b7707dcff29d726474b3292a261b Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:50:18 -0600 Subject: [PATCH 09/17] Remove outdated examples and improve documentation clarity for EDID data retrieval --- DDCSwitch/Monitor.cs | 3 -- EXAMPLES.md | 111 +------------------------------------------ README.md | 12 ----- 3 files changed, 1 insertion(+), 125 deletions(-) diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 5e9a6e6..2061fa6 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; namespace DDCSwitch; diff --git a/EXAMPLES.md b/EXAMPLES.md index b90b2d6..dc77bd8 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -14,40 +14,6 @@ View all EDID data for a specific monitor: ddcswitch info 0 ``` -Example output: -``` -── Monitor 0: Generic PnP Monitor ─────────────────────────────── - Device Name: \\.\DISPLAY2 - Is Primary: No - - EDID Information - EDID Version: 1.4 - Manufacturer ID: ACR - Manufacturer: Acer Technologies - Model Name: VG270U P - Product Code: 0x06CF - Manufacture Date: 2021 Week 2 - Video Input Type: Digital - - Supported Features - Display Type: RGB Color - DPMS Standby: Supported - DPMS Suspend: Not supported - DPMS Active-Off: Supported - Default Color Space: Standard - Preferred Timing Mode: Included - Continuous Frequency: Supported - - Chromaticity Coordinates (CIE 1931) - Red: x=0.6396, y=0.3300 - Green: x=0.2998, y=0.5996 - Blue: x=0.1503, y=0.0595 - White Point: x=0.3125, y=0.3291 - - Current Status - Current Input: HDMI1 (0x11) -``` - ### JSON Output for EDID Data Retrieve EDID data in JSON format for programmatic access: @@ -56,49 +22,6 @@ Retrieve EDID data in JSON format for programmatic access: ddcswitch info 0 --json ``` -Example JSON output: -```json -{ - "success": true, - "monitor": { - "index": 0, - "name": "Generic PnP Monitor", - "deviceName": "\\\\.\\DISPLAY2", - "isPrimary": false - }, - "status": "ok", - "currentInput": "HDMI1", - "currentInputCode": "0x11", - "edid": { - "versionMajor": 1, - "versionMinor": 4, - "manufacturerId": "ACR", - "manufacturerName": "Acer Technologies", - "modelName": "VG270U P", - "productCode": "0x06CF", - "manufactureYear": 2021, - "manufactureWeek": 2, - "isDigitalInput": true, - "videoInputType": "Digital", - "features": { - "displayType": "RGB Color", - "dpmsStandby": false, - "dpmsSuspend": false, - "dpmsActiveOff": true, - "defaultColorSpace": false, - "preferredTimingMode": true, - "continuousFrequency": true - }, - "chromaticity": { - "red": { "x": 0.6396484375, "y": 0.330078125 }, - "green": { "x": 0.2998046875, "y": 0.599609375 }, - "blue": { "x": 0.150390625, "y": 0.0595703125 }, - "white": { "x": 0.3125, "y": 0.32910156 } - } - } -} -``` - ### Automation Examples with EDID Data #### PowerShell: Check Monitor Model Before Applying Settings @@ -214,39 +137,7 @@ inventoryMonitors(); ## Comprehensive VCP Feature Examples -ddcswitch now supports all MCCS (Monitor Control Command Set) standardized VCP features, organized by categories for easy discovery. - -### VCP Feature Categories - -List all available feature categories: - -```powershell -ddcswitch list --categories -``` - -Output: -``` -Available VCP Feature Categories: -- ImageAdjustment: Brightness, contrast, sharpness, backlight controls -- ColorControl: RGB gains, color temperature, gamma, hue, saturation -- Geometry: Position, size, pincushion controls (mainly CRT monitors) -- Audio: Volume, mute, balance, treble, bass controls -- Preset: Factory defaults, degauss, calibration features -- Miscellaneous: Power mode, OSD settings, firmware information -``` - -### Browse Features by Category - -```powershell -# Image adjustment features -ddcswitch list --category image - -# Color control features -ddcswitch list --category color - -# Audio features -ddcswitch list --category audio -``` +ddcswitch now supports all MCCS (Monitor Control Command Set) standardized VCP features. ### Color Control Examples diff --git a/README.md b/README.md index 547221c..4e01591 100644 --- a/README.md +++ b/README.md @@ -194,18 +194,6 @@ The toggle command detects the current input and switches to the alternate one: Perfect for hotkeys and automation where you want to switch between two specific inputs without knowing which one is currently active. -Set brightness or contrast with percentage values: - -```powershell -# Set brightness to 75% -ddcswitch set 0 brightness 75% - -# Set contrast to 80% -ddcswitch set 0 contrast 80% -``` - -Output: `✓ Successfully set brightness to 75% (120/160)` - ### Raw VCP Access For advanced users, access any VCP feature by code: From 4efcd89b1d695282ccbc38797aeea2f9ea085eef Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:53:25 -0600 Subject: [PATCH 10/17] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 547221c..c60bc4c 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display D ## Features - 🖥️ **List all DDC/CI capable monitors** with their current input sources -- � **Detailed EDID information** - View monitor specifications, capabilities, and color characteristics -- �🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) +- **Detailed EDID information** - View monitor specifications, capabilities, and color characteristics +- 🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) - 🔆 **Control brightness and contrast** with percentage values (0-100%) - 🎛️ **Comprehensive VCP feature support** - Access all MCCS standardized monitor controls - 🏷️ **Feature categories and discovery** - Browse VCP features by category (Image, Color, Geometry, Audio, etc.) From a474f12ece4afe0c7ce89fb5f9fbf6abf99b9e00 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:53:35 -0600 Subject: [PATCH 11/17] Update DDCSwitch/Commands/ToggleCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DDCSwitch/Commands/ToggleCommand.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/DDCSwitch/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs index e3dc3d3..e2c99aa 100644 --- a/DDCSwitch/Commands/ToggleCommand.cs +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -88,8 +88,6 @@ private static int HandleMissingArguments(int actualCount, bool jsonOutput) return 1; } - - private static int HandleInvalidInput(string invalidInput, string position, bool jsonOutput) { string errorMessage = $"Invalid {position} input source: '{invalidInput}'"; From 9fe5f4b302a81e7096be83190f2abcc2b19696c4 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:54:06 -0600 Subject: [PATCH 12/17] Update DDCSwitch/NativeMethods.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DDCSwitch/NativeMethods.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index bd3810b..da45c42 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -147,8 +147,8 @@ public struct MONITORINFOEX return edidList[listIndex]; } - // Fallback: return first EDID if index doesn't match - return edidList.Count > 0 ? edidList[0] : null; + // No reliable EDID mapping found for this display index + return null; } catch (System.Security.SecurityException) { From 0af6a6d56045ed1c5abf4146eaa04a165b368643 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:54:32 -0600 Subject: [PATCH 13/17] Update DDCSwitch/EdidParser.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DDCSwitch/EdidParser.cs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs index 344a598..00b182c 100644 --- a/DDCSwitch/EdidParser.cs +++ b/DDCSwitch/EdidParser.cs @@ -490,28 +490,24 @@ public static bool ValidateHeader(byte[] edid) { // Chromaticity data is stored in bytes 25-34 // Each coordinate is a 10-bit value split between two bytes - byte lsb = edid[25]; // Low-order bits for red/green X - byte lsb2 = edid[26]; // Low-order bits for red/green Y + byte lsb = edid[25]; // Low-order bits for red/green/blue/white X + byte lsb2 = edid[26]; // Low-order bits for red/green/blue/white Y - // Red X: bits 7-6 of byte 27 (MSB) + all 8 bits of byte 25 bits 1-0 (LSB) - // Red Y: bits 5-4 of byte 27 (MSB) + all 8 bits of byte 26 bits 1-0 (LSB) - int redXRaw = ((edid[27] & 0xC0) << 2) | ((lsb >> 6) & 0x03); - int redYRaw = ((edid[27] & 0x30) << 4) | ((lsb2 >> 6) & 0x03); + // Red X/Y: 8 MSB bits in byte 27, 2 LSB bits in bytes 25/26 + int redXRaw = (edid[27] << 2) | ((lsb >> 6) & 0x03); + int redYRaw = (edid[27] << 2) | ((lsb2 >> 6) & 0x03); - // Green X: bits 7-6 of byte 28 + byte 25 bits 5-4 - // Green Y: bits 5-4 of byte 28 + byte 26 bits 5-4 - int greenXRaw = ((edid[28] & 0xC0) << 2) | ((lsb >> 4) & 0x03); - int greenYRaw = ((edid[28] & 0x30) << 4) | ((lsb2 >> 4) & 0x03); + // Green X/Y: 8 MSB bits in byte 28, 2 LSB bits in bytes 25/26 + int greenXRaw = (edid[28] << 2) | ((lsb >> 4) & 0x03); + int greenYRaw = (edid[28] << 2) | ((lsb2 >> 4) & 0x03); - // Blue X: bits 7-6 of byte 29 + byte 25 bits 3-2 - // Blue Y: bits 5-4 of byte 29 + byte 26 bits 3-2 - int blueXRaw = ((edid[29] & 0xC0) << 2) | ((lsb >> 2) & 0x03); - int blueYRaw = ((edid[29] & 0x30) << 4) | ((lsb2 >> 2) & 0x03); + // Blue X/Y: 8 MSB bits in byte 29, 2 LSB bits in bytes 25/26 + int blueXRaw = (edid[29] << 2) | ((lsb >> 2) & 0x03); + int blueYRaw = (edid[29] << 2) | ((lsb2 >> 2) & 0x03); - // White X: bits 7-6 of byte 30 + byte 25 bits 1-0 - // White Y: bits 5-4 of byte 30 + byte 26 bits 1-0 - int whiteXRaw = ((edid[30] & 0xC0) << 2) | (lsb & 0x03); - int whiteYRaw = ((edid[30] & 0x30) << 4) | (lsb2 & 0x03); + // White X/Y: 8 MSB bits in byte 30, 2 LSB bits in bytes 25/26 + int whiteXRaw = (edid[30] << 2) | (lsb & 0x03); + int whiteYRaw = (edid[30] << 2) | (lsb2 & 0x03); // Convert 10-bit values to 0.0-1.0 range var red = new ColorPoint(redXRaw / 1024.0, redYRaw / 1024.0); From c171cda38217163eb875ae63e2853a4de4e94def Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:08:11 -0600 Subject: [PATCH 14/17] Align table columns in ConsoleOutputFormatter and adjust spacing in ListCommand output --- DDCSwitch/Commands/ConsoleOutputFormatter.cs | 6 +++--- DDCSwitch/Commands/ListCommand.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 97d6e2d..2afe711 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -152,9 +152,9 @@ public static void WriteMonitorDetails(Monitor monitor) var chromaTable = new Table() .Border(TableBorder.Rounded) .BorderColor(Color.Grey) - .AddColumn(new TableColumn("[bold]Color[/]").Centered().Width(12)) - .AddColumn(new TableColumn("[bold]X[/]").Centered().Width(12)) - .AddColumn(new TableColumn("[bold]Y[/]").Centered().Width(12)); + .AddColumn(new TableColumn("[bold]Color[/]").LeftAligned().Width(12)) + .AddColumn(new TableColumn("[bold]X[/]").LeftAligned().Width(12)) + .AddColumn(new TableColumn("[bold]Y[/]").LeftAligned().Width(12)); chromaTable.AddRow( "[red]● Red[/]", diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs index f00371c..771f9d5 100644 --- a/DDCSwitch/Commands/ListCommand.cs +++ b/DDCSwitch/Commands/ListCommand.cs @@ -224,7 +224,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) var row = new List { - monitor.IsPrimary ? $"[bold cyan]{monitor.Index}[/] [yellow]*[/]" : $"[cyan]{monitor.Index}[/]", + monitor.IsPrimary ? $"[bold cyan] {monitor.Index}[/][yellow]*[/]" : $"[cyan]{monitor.Index}[/]", monitor.Name, $"[dim]{monitor.DeviceName}[/]", inputInfo From 5387633b181235c538fbc33675ef985f75e10018 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:19:25 -0600 Subject: [PATCH 15/17] Add build script and enhance command output styling - Introduced a PowerShell build script for NativeAOT compilation - Improved visual styling across commands with enhanced panels, icons, and progress indicators - Updated README with installation instructions and usage examples - Enhanced error and success messages for better user feedback --- CHANGELOG.md | 18 ++ DDCSwitch/Commands/GetCommand.cs | 43 +++-- DDCSwitch/Commands/InfoCommand.cs | 2 +- DDCSwitch/Commands/ListCommand.cs | 41 ++++- DDCSwitch/Commands/SetCommand.cs | 21 ++- DDCSwitch/Commands/ToggleCommand.cs | 17 +- DDCSwitch/Commands/VcpScanCommand.cs | 57 ++++-- README.md | 259 ++++++--------------------- build.cmd | 53 ------ build.ps1 | 47 +++++ 10 files changed, 254 insertions(+), 304 deletions(-) delete mode 100644 build.cmd create mode 100644 build.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a2904b0..00a8bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to ddcswitch will be documented in this file. +## [1.0.4] - 2026-01-09 + +### Added +- Enhanced visual styling across all commands with improved panels, tables, and progress bars +- Better color hierarchy and readability optimized for Windows Terminal and Command Prompt dark backgrounds +- Improved status indicators with clear symbols (✓/✗/⚠) for monitor and feature status +- Visual progress bars for percentage-based features (brightness, contrast) in `get` command +- Summary statistics in `list` command showing monitor count and DDC/CI status overview +- Better visual separation and formatting in `set`, `toggle`, and scan commands +- Documentation for `NO_COLOR` environment variable support (automatic via Spectre.Console) +- Consistent panel styling with icons for better visual hierarchy (📋⚡🎨📡) + +### Improved +- Error messages now include better formatting and actionable hints +- Success panels show more detailed information with clear visual structure +- Table styling enhanced for better readability on dark backgrounds +- Warning messages have improved formatting and context + ## [1.0.3] - 2026-01-08 ### Added diff --git a/DDCSwitch/Commands/GetCommand.cs b/DDCSwitch/Commands/GetCommand.cs index 7c6914d..c947f09 100644 --- a/DDCSwitch/Commands/GetCommand.cs +++ b/DDCSwitch/Commands/GetCommand.cs @@ -1,4 +1,4 @@ -using Spectre.Console; +using Spectre.Console; using System.Text.Json; namespace DDCSwitch.Commands; @@ -267,39 +267,58 @@ private static void OutputFeatureValue(Monitor monitor, VcpFeature feature, uint } else { - var panel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + + // Header panel with monitor info + var headerPanel = new Panel( + $"[bold white]{monitor.Name}[/]\n" + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]") { - Header = new PanelHeader($"[bold green]>> Feature Value[/]", Justify.Left), + Header = new PanelHeader($"[bold cyan]📊 {feature.Name}[/]", Justify.Left), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Cyan) }; - AnsiConsole.Write(panel); + AnsiConsole.Write(headerPanel); + AnsiConsole.WriteLine(); if (feature.Code == InputSource.VcpInputSource) { // Display input with name resolution var inputName = InputSource.GetName(current); - AnsiConsole.MarkupLine($" [bold yellow]{feature.Name}:[/] [cyan]{inputName}[/] [dim](0x{current:X2})[/]"); + var valuePanel = new Panel( + $"[bold cyan]{inputName}[/] [dim](0x{current:X2})[/]") + { + Header = new PanelHeader("[bold yellow]Current Input Source[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(valuePanel); } else if (feature.SupportsPercentage) { - // Display percentage for brightness/contrast + // Display percentage for brightness/contrast with progress bar uint percentage = FeatureResolver.ConvertRawToPercentage(current, max); + var progressBar = new BarChart() - .Width(40) - .Label($"[bold yellow]{feature.Name}[/]") + .Width(50) + .Label($"[bold yellow]{percentage}%[/]") .CenterLabel() - .AddItem("", percentage, Color.Green); + .AddItem("", percentage, percentage >= 75 ? Color.Green : + percentage >= 50 ? Color.Yellow : + percentage >= 25 ? Color.Orange1 : Color.Red); AnsiConsole.Write(progressBar); - AnsiConsole.MarkupLine($" [bold green]{percentage}%[/] [dim](raw: {current}/{max})[/]"); + AnsiConsole.MarkupLine($" [dim]Raw value: {current}/{max}[/]"); } else { // Display raw values for unknown VCP codes - AnsiConsole.MarkupLine($" [bold yellow]{feature.Name}:[/] [green]{current}[/] [dim](max: {max})[/]"); + var valuePanel = new Panel( + $"[bold green]{current}[/] [dim](maximum: {max})[/]") + { + Header = new PanelHeader("[bold yellow]Current Value[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(valuePanel); } } } diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs index 36e431e..79fbcd8 100644 --- a/DDCSwitch/Commands/InfoCommand.cs +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -204,7 +204,7 @@ private static int HandleMonitorNotFound(List monitors, string identifi else { ConsoleOutputFormatter.WriteError($"Monitor '{identifier}' not found."); - AnsiConsole.MarkupLine($"Available monitors: [cyan]{string.Join(", ", monitors.Select(m => m.Index.ToString()))}[/]"); + AnsiConsole.MarkupLine("[dim]Use [/][yellow]ddcswitch list[/][dim] to see available monitors.[/]"); } foreach (var m in monitors) diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs index 771f9d5..db5b439 100644 --- a/DDCSwitch/Commands/ListCommand.cs +++ b/DDCSwitch/Commands/ListCommand.cs @@ -1,4 +1,4 @@ -using Spectre.Console; +using Spectre.Console; using System.Text.Json; namespace DDCSwitch.Commands; @@ -141,6 +141,35 @@ private static void OutputJsonList(List monitors, bool verboseOutput) private static void OutputTableList(List monitors, bool verboseOutput) { + // Summary statistics header + int ddcciCount = 0; + int primaryCount = 0; + + foreach (var monitor in monitors) + { + if (monitor.IsPrimary) primaryCount++; + try + { + if (monitor.TryGetInputSource(out _, out _)) + { + ddcciCount++; + } + } + catch { } + } + + var summaryPanel = new Panel( + $"[bold white]Total Monitors:[/] [cyan]{monitors.Count}[/] " + + $"[bold white]DDC/CI Capable:[/] [green]{ddcciCount}[/] " + + $"[bold white]Primary:[/] [yellow]{primaryCount}[/]") + { + Header = new PanelHeader("[bold cyan]🖥️ Monitor Overview[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Cyan) + }; + AnsiConsole.Write(summaryPanel); + AnsiConsole.WriteLine(); + var table = new Table() .Border(TableBorder.Rounded) .BorderColor(Color.White) @@ -165,7 +194,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) foreach (var monitor in monitors) { string inputInfo = "[dim]N/A[/]"; - string status = "[green]+[/] [bold green]OK[/]"; + string status = "[green]✓[/] [bold green]OK[/]"; string brightnessInfo = "[dim]N/A[/]"; string contrastInfo = "[dim]N/A[/]"; @@ -178,11 +207,11 @@ private static void OutputTableList(List monitors, bool verboseOutput) } else { - status = "[yellow]~[/] [bold yellow]No DDC/CI[/]"; + status = "[yellow]⚠[/] [bold yellow]No DDC/CI[/]"; } // Get brightness and contrast if verbose mode is enabled and monitor supports DDC/CI - if (verboseOutput && status == "[green]+[/] [bold green]OK[/]") + if (verboseOutput && status == "[green]✓[/] [bold green]OK[/]") { // Try to get brightness (VCP 0x10) if (monitor.TryGetVcpFeature(VcpFeature.Brightness.Code, out uint brightnessCurrent, out uint brightnessMax)) @@ -214,7 +243,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) } catch { - status = "[red]X[/] [bold red]Error[/]"; + status = "[red]✗[/] [bold red]Error[/]"; if (verboseOutput) { brightnessInfo = "[dim]N/A[/]"; @@ -224,7 +253,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) var row = new List { - monitor.IsPrimary ? $"[bold cyan] {monitor.Index}[/][yellow]*[/]" : $"[cyan]{monitor.Index}[/]", + monitor.IsPrimary ? $"[bold cyan]{monitor.Index}[/] [yellow]●[/]" : $"[cyan]{monitor.Index}[/]", monitor.Name, $"[dim]{monitor.DeviceName}[/]", inputInfo diff --git a/DDCSwitch/Commands/SetCommand.cs b/DDCSwitch/Commands/SetCommand.cs index ce4b6c2..c6b73d8 100644 --- a/DDCSwitch/Commands/SetCommand.cs +++ b/DDCSwitch/Commands/SetCommand.cs @@ -1,4 +1,4 @@ -using Spectre.Console; +using Spectre.Console; using System.Text.Json; namespace DDCSwitch.Commands; @@ -369,28 +369,33 @@ private static void OutputSuccess(Monitor monitor, VcpFeature feature, uint setV else { string displayValue; + string featureIcon; + if (feature.Code == InputSource.VcpInputSource) { // Display input with name resolution - displayValue = $"[cyan]{InputSource.GetName(setValue)}[/]"; + displayValue = $"[bold cyan]{InputSource.GetName(setValue)}[/] [dim](0x{setValue:X2})[/]"; + featureIcon = "📺"; } else if (percentageValue.HasValue) { // Display percentage for brightness/contrast - displayValue = $"[green]{percentageValue}%[/]"; + displayValue = $"[bold green]{percentageValue}%[/] [dim](raw: {setValue})[/]"; + featureIcon = feature.Code == VcpFeature.Brightness.Code ? "☀️" : "🎨"; } else { // Display raw value for unknown VCP codes - displayValue = $"[green]{setValue}[/]"; + displayValue = $"[bold green]{setValue}[/]"; + featureIcon = "⚙️"; } var successPanel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + - $"[bold yellow]Feature:[/] {feature.Name}\n" + - $"[bold green]New Value:[/] {displayValue}") + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n\n" + + $"[bold yellow]{feature.Name}:[/] {displayValue}") { - Header = new PanelHeader("[bold green]>> Successfully Applied[/]", Justify.Left), + Header = new PanelHeader($"[bold green]✓ {featureIcon} Successfully Applied[/]", Justify.Left), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Green) }; diff --git a/DDCSwitch/Commands/ToggleCommand.cs b/DDCSwitch/Commands/ToggleCommand.cs index e2c99aa..0e9cf77 100644 --- a/DDCSwitch/Commands/ToggleCommand.cs +++ b/DDCSwitch/Commands/ToggleCommand.cs @@ -379,11 +379,11 @@ private static void OutputToggleSuccess(Monitor monitor, uint currentInput, uint else { var successPanel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + - $"[bold yellow]From:[/] {fromInputName}\n" + - $"[bold green]To:[/] {toInputName}") + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n\n" + + $"[cyan]{fromInputName}[/] [dim](0x{currentInput:X2})[/] [bold yellow]→[/] [bold cyan]{toInputName}[/] [dim](0x{targetInput:X2})[/]") { - Header = new PanelHeader("[bold green]>> Input Toggled Successfully[/]", Justify.Left), + Header = new PanelHeader("[bold green]✓ 🔄 Input Toggled Successfully[/]", Justify.Left), Border = BoxBorder.Rounded, BorderStyle = new Style(Color.Green) }; @@ -392,7 +392,14 @@ private static void OutputToggleSuccess(Monitor monitor, uint currentInput, uint if (hasWarning && warningMessage != null) { - AnsiConsole.MarkupLine($"[yellow]Warning:[/] {warningMessage}"); + AnsiConsole.WriteLine(); + var warningPanel = new Panel(warningMessage) + { + Header = new PanelHeader("[bold yellow]⚠ Warning[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Yellow) + }; + AnsiConsole.Write(warningPanel); } } } diff --git a/DDCSwitch/Commands/VcpScanCommand.cs b/DDCSwitch/Commands/VcpScanCommand.cs index b2466ae..863de53 100644 --- a/DDCSwitch/Commands/VcpScanCommand.cs +++ b/DDCSwitch/Commands/VcpScanCommand.cs @@ -199,7 +199,8 @@ private static void OutputTableScanAll(List monitors) Style = new Style(Color.Cyan) }; AnsiConsole.Write(rule); - + AnsiConsole.WriteLine(); + Dictionary features = null!; AnsiConsole.Status() .Start($"Scanning VCP features for {monitor.Name}...", ctx => @@ -221,7 +222,15 @@ private static void OutputTableScanAll(List monitors) continue; } - AnsiConsole.MarkupLine($"[bold green]>> Found {supportedFeatures.Count} supported features[/]\n"); + // Show feature count summary + int readWriteCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadWrite); + int readOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadOnly); + int writeOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.WriteOnly); + + AnsiConsole.MarkupLine( + $"[bold green]✓ Found {supportedFeatures.Count} supported features[/] " + + $"[dim]([green]{readWriteCount}[/] R/W, [yellow]{readOnlyCount}[/] R, [red]{writeOnlyCount}[/] W)[/]\n"); + OutputFeatureTable(supportedFeatures); AnsiConsole.WriteLine(); } @@ -241,16 +250,23 @@ private static void OutputJsonScanSingle(MonitorReference monitorRef, List supportedFeatures) { - var panel = new Panel( - $"[bold cyan]Monitor:[/] {monitor.Name}\n" + - $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n" + - $"[bold yellow]Supported Features:[/] [green]{supportedFeatures.Count}[/]") + // Summary panel with feature count breakdown + int readWriteCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadWrite); + int readOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.ReadOnly); + int writeOnlyCount = supportedFeatures.Count(f => f.Type == VcpFeatureType.WriteOnly); + + var summaryPanel = new Panel( + $"[bold white]{monitor.Name}[/]\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]\n\n" + + $"[bold yellow]Total Features:[/] [green]{supportedFeatures.Count}[/] " + + $"[dim]([green]{readWriteCount}[/] R/W, [yellow]{readOnlyCount}[/] R, [red]{writeOnlyCount}[/] W)[/]") { - Header = new PanelHeader($"[bold cyan]>> VCP Feature Scan Results[/]", Justify.Left), + Header = new PanelHeader($"[bold cyan]🔍 VCP Feature Scan Results[/]", Justify.Left), Border = BoxBorder.Rounded, - BorderStyle = new Style(Color.White) + BorderStyle = new Style(Color.Cyan) }; - AnsiConsole.Write(panel); + AnsiConsole.Write(summaryPanel); + AnsiConsole.WriteLine(); if (supportedFeatures.Count == 0) { @@ -266,7 +282,7 @@ private static void OutputFeatureTable(List supportedFeatures) { var table = new Table() .Border(TableBorder.Rounded) - .BorderColor(Color.White) + .BorderColor(Color.Grey) .AddColumn(new TableColumn("[bold yellow]VCP Code[/]").Centered()) .AddColumn(new TableColumn("[bold yellow]Feature Name[/]").LeftAligned()) .AddColumn(new TableColumn("[bold yellow]Access[/]").Centered()) @@ -278,10 +294,10 @@ private static void OutputFeatureTable(List supportedFeatures) string vcpCode = $"[cyan]0x{feature.Code:X2}[/]"; string accessType = feature.Type switch { - VcpFeatureType.ReadOnly => "[yellow]Read Only[/]", - VcpFeatureType.WriteOnly => "[red]Write Only[/]", - VcpFeatureType.ReadWrite => "[green]Read+Write[/]", - _ => "[dim]? ?[/]" + VcpFeatureType.ReadOnly => "[yellow]✓[/] [dim]Read[/]", + VcpFeatureType.WriteOnly => "[red]✎[/] [dim]Write[/]", + VcpFeatureType.ReadWrite => "[green]✓✎[/] [dim]R/W[/]", + _ => "[dim]?[/]" }; string currentValue = $"[green]{feature.CurrentValue}[/]"; @@ -294,6 +310,19 @@ private static void OutputFeatureTable(List supportedFeatures) { name = $"[dim]{feature.Name}[/]"; } + else + { + // Add icon for common features + name = feature.Code switch + { + 0x10 => $"☀️ {feature.Name}", + 0x12 => $"🎨 {feature.Name}", + 0x60 => $"📺 {feature.Name}", + 0x62 => $"🔊 {feature.Name}", + 0x8D => $"🔇 {feature.Name}", + _ => feature.Name + }; + } table.AddRow(vcpCode, name, accessType, currentValue, maxValue); } diff --git a/README.md b/README.md index b1cc3c2..47a72a3 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,23 @@ A Windows command-line utility to control monitor settings via DDC/CI (Display Data Channel Command Interface). Control input sources, brightness, contrast, and other VCP features without touching physical buttons. +The project is pre-configured with NativeAOT, which produces a native executable with instant startup and no .NET runtime dependency. + 📚 **[Examples](EXAMPLES.md)** | 📝 **[Changelog](CHANGELOG.md)** ## Features - 🖥️ **List all DDC/CI capable monitors** with their current input sources -- **Detailed EDID information** - View monitor specifications, capabilities, and color characteristics +- 🔍 **EDID information** - View monitor specifications, capabilities, and color characteristics - 🔄 **Switch monitor inputs** programmatically (HDMI, DisplayPort, DVI, VGA, etc.) - 🔆 **Control brightness and contrast** with percentage values (0-100%) - 🎛️ **Comprehensive VCP feature support** - Access all MCCS standardized monitor controls -- 🏷️ **Feature categories and discovery** - Browse VCP features by category (Image, Color, Geometry, Audio, etc.) - 🔍 **VCP scanning** to discover all supported monitor features - 🎯 **Simple CLI interface** perfect for scripts, shortcuts, and hotkeys - 📊 **JSON output support** - Machine-readable output for automation and integration - ⚡ **Fast and lightweight** - NativeAOT compiled for instant startup - 📦 **True native executable** - No .NET runtime dependency required -- 🪟 **Windows-only** - uses native Windows DDC/CI APIs (use ddcutil on Linux) +- 🪟 **Windows-only** - uses native Windows DDC/CI APIs (use [ddcutil](https://www.ddcutil.com/) on Linux) ## Installation @@ -43,7 +44,16 @@ choco upgrade ddcswitch ### Pre-built Binary -Download the latest release from the [Releases](../../releases) page and extract `ddcswitch.exe` to a folder in your PATH. +Download the latest release from the [Releases](../../releases) page and extract `ddcswitch.exe` to a folder in your `PATH`. + +#### How to add to PATH: +1. Copy `ddcswitch.exe` to a folder (e.g., `C:\Tools\ddcswitch\`). +2. Open Start Menu, search "Environment Variables", and select "Edit the system environment variables" +3. Click "Environment Variables..." +4. Under "System variables", select "Path" and click "Edit..." +5. Click "New" and add the folder path (e.g., `C:\Tools\ddcswitch\`) +6. Click OK on all dialogs to apply changes. +7. Restart any open command prompts or PowerShell windows. ### Build from Source @@ -60,12 +70,13 @@ cd ddcswitch dotnet publish -c Release ``` -The project is pre-configured with NativeAOT (`true`), which produces a ~3-5 MB native executable with instant startup and no .NET runtime dependency. - Executable location: `ddcswitch/bin/Release/net10.0/win-x64/publish/ddcswitch.exe` ## Usage +[!NOTE] +JSON output is supported with `--json` flag all data and commands. + ### List Monitors Display all DDC/CI capable monitors with their current input sources: @@ -74,36 +85,6 @@ Display all DDC/CI capable monitors with their current input sources: ddcswitch list ``` -Example output: -``` -╭───────┬─────────────────────┬──────────────┬───────────────────────────┬────────╮ -│ Index │ Monitor Name │ Device │ Current Input │ Status │ -├───────┼─────────────────────┼──────────────┼───────────────────────────┼────────┤ -│ 0 │ Generic PnP Monitor │ \\.\DISPLAY2 │ HDMI1 (0x11) │ OK │ -│ 1* │ VG270U P │ \\.\DISPLAY1 │ DisplayPort1 (DP1) (0x0F) │ OK │ -╰───────┴─────────────────────┴──────────────┴───────────────────────────┴────────╯ -``` - -#### Verbose Listing - -Add `--verbose` to include brightness and contrast information: - -```powershell -ddcswitch list --verbose -``` - -Example output: -``` -╭───────┬─────────────────────┬──────────────┬───────────────────────────┬────────┬────────────┬──────────╮ -│ Index │ Monitor Name │ Device │ Current Input │ Status │ Brightness │ Contrast │ -├───────┼─────────────────────┼──────────────┼───────────────────────────┼────────┼────────────┼──────────┤ -│ 0 │ Generic PnP Monitor │ \\.\DISPLAY2 │ HDMI1 (0x11) │ OK │ 75% │ 80% │ -│ 1* │ VG270U P │ \\.\DISPLAY1 │ DisplayPort1 (DP1) (0x0F) │ OK │ N/A │ N/A │ -╰───────┴─────────────────────┴──────────────┴───────────────────────────┴────────┴────────────┴──────────╯ -``` - -Add `--json` for machine-readable output (see [EXAMPLES.md](EXAMPLES.md) for automation examples). - ### Monitor Information (EDID) View detailed EDID (Extended Display Identification Data) information for a specific monitor: @@ -112,72 +93,33 @@ View detailed EDID (Extended Display Identification Data) information for a spec ddcswitch info 0 ``` -The info command provides comprehensive monitor details including: -- **EDID version** and manufacturer information -- **Model name**, serial number, and manufacture date -- **Video input type** (Digital/Analog) -- **Supported features** (DPMS power modes, display type, color space) -- **Chromaticity coordinates** for color calibration (red, green, blue, white points in CIE 1931 color space) -- **Current input source** status - -JSON output is supported with `--json` flag for programmatic access to all EDID data: - -```powershell -ddcswitch info 0 --json -``` - ### Get Current Settings -Get all VCP features for a specific monitor: - -```powershell -ddcswitch get 0 -``` - -This will scan and display all supported VCP features for monitor 0, showing their names, access types, current values, and maximum values. - -You can also use the monitor name instead of the index (partial name matching supported): +Get a specific feature by monitor index or name: ```powershell -# Get all settings by monitor name -ddcswitch get "VG270U P" -ddcswitch get "Generic PnP" -``` - -Get a specific feature: - -```powershell -# Get current input source -ddcswitch get 0 input - -# Get brightness as percentage +# Get brightness by monitor index ddcswitch get 0 brightness -# Get contrast as percentage -ddcswitch get 0 contrast - -# Works with monitor names too -ddcswitch get "VG270U P" brightness -ddcswitch get "Generic PnP" input +# Get input source by monitor name (partial match supported) +ddcswitch get "VG270U" input ``` -Output: `Monitor: Generic PnP Monitor` / `Brightness: 75% (120/160)` - ### Set Monitor Settings -Switch a monitor to a different input: +Set brightness, contrast, or switch inputs by monitor index or name: ```powershell -# By monitor index -ddcswitch set 0 HDMI1 +# Set brightness by index +ddcswitch set 0 brightness 75% -# By monitor name (partial match) +# Switch input by monitor name (partial match supported) ddcswitch set "LG ULTRAGEAR" HDMI2 ``` ### Toggle Between Input Sources -Automatically switch between two input sources without specifying which one: +Automatically switch between two input sources: ```powershell # Toggle between HDMI1 and DisplayPort1 @@ -187,12 +129,7 @@ ddcswitch toggle 0 HDMI1 DP1 ddcswitch toggle "LG ULTRAGEAR" HDMI1 HDMI2 ``` -The toggle command detects the current input and switches to the alternate one: -- If current input is HDMI1 → switches to DP1 -- If current input is DP1 → switches to HDMI1 -- If current input is neither → switches to HDMI1 (with warning) - -Perfect for hotkeys and automation where you want to switch between two specific inputs without knowing which one is currently active. +The toggle command detects the current input and switches to the alternate one - perfect for hotkeys and automation. ### Raw VCP Access @@ -226,33 +163,6 @@ ddcswitch get 0 ddcswitch get "VG270U" ``` -### VCP Feature Categories and Discovery - -Discover and browse VCP features by category: - -```powershell -# List all available categories -ddcswitch list --categories - -# List features in a specific category -ddcswitch list --category image -ddcswitch list --category color -ddcswitch list --category audio -``` - -Example output: -``` -Image Adjustment Features: -- brightness (0x10): Brightness control -- contrast (0x12): Contrast control -- sharpness (0x87): Sharpness control -- backlight (0x13): Backlight control - -Color Control Features: -- red-gain (0x16): Video gain: Red -- green-gain (0x18): Video gain: Green -- blue-gain (0x1A): Video gain: Blue -``` ### Supported Features @@ -273,102 +183,55 @@ Color Control Features: - **Geometry**: `h-position`, `v-position`, `clock`, `phase` (mainly for CRT monitors) - **Presets**: `restore-defaults`, `degauss` (VCP 0x04, 0x01) -#### VCP Feature Categories -- **Image Adjustment**: brightness, contrast, sharpness, backlight, etc. -- **Color Control**: RGB gains, color temperature, gamma, hue, saturation -- **Geometry**: position, size, pincushion controls (mainly CRT) -- **Audio**: volume, mute, balance, treble, bass -- **Preset**: factory defaults, degauss, calibration -- **Miscellaneous**: power mode, OSD settings, firmware info #### Raw VCP Codes - Any VCP code from `0x00` to `0xFF` - Values must be within the monitor's supported range - Use hex format: `0x10`, `0x12`, etc. -## Use Cases +## Quick Start -### Quick Examples +### Basic Usage Examples -**Switch multiple monitors:** ```powershell -ddcswitch set 0 HDMI1 -ddcswitch set 1 DP1 -``` - -**Toggle between input sources:** -```powershell -# Toggle main monitor between HDMI1 and DisplayPort -ddcswitch toggle 0 HDMI1 DP1 +# List monitors +ddcswitch list -# Toggle secondary monitor between HDMI inputs -ddcswitch toggle 1 HDMI1 HDMI2 -``` +# Switch monitor input +ddcswitch set 0 HDMI1 -**Control comprehensive VCP features:** -```powershell +# Adjust brightness ddcswitch set 0 brightness 75% -ddcswitch set 0 contrast 80% -ddcswitch get 0 brightness - -# Color controls -ddcswitch set 0 red-gain 90% -ddcswitch set 0 green-gain 85% -ddcswitch set 0 blue-gain 95% - -# Audio controls (if supported) -ddcswitch set 0 volume 50% -ddcswitch set 0 mute 1 ``` -**VCP feature discovery:** -```powershell -# List all available VCP feature categories -ddcswitch list --categories - -# List features in a specific category -ddcswitch list --category color +### JSON Output -# Search for features by name -ddcswitch get 0 bright # Matches "brightness" +All commands support `--json` for machine-readable output, perfect for automation: -# Or by monitor name -ddcswitch get "VG270U" bright -``` - -**Desktop shortcut:** -Create a shortcut with target: `C:\Path\To\ddcswitch.exe set 0 brightness 50%` +```powershell +# Get monitor list as JSON +ddcswitch list --json -**AutoHotkey:** -```autohotkey -^!h::Run, ddcswitch.exe set 0 HDMI1 ; Ctrl+Alt+H for HDMI1 -^!d::Run, ddcswitch.exe set 0 DP1 ; Ctrl+Alt+D for DisplayPort -^!b::Run, ddcswitch.exe set 0 brightness 75% ; Ctrl+Alt+B for 75% brightness +# Get specific monitor info as JSON +ddcswitch info 0 --json ``` -### JSON Output for Automation +### Plain Text Output -All commands support `--json` for machine-readable output: +To disable colors and icons (for logging or automation), set the `NO_COLOR` environment variable: ```powershell -# PowerShell: Conditional switching -$result = ddcswitch get 0 --json | ConvertFrom-Json -if ($result.currentInputCode -ne "0x11") { - ddcswitch set 0 HDMI1 -} +$env:NO_COLOR = "1" +ddcswitch list ``` -```python -# Python: Switch all monitors -import subprocess, json -data = json.loads(subprocess.run(['ddcswitch', 'list', '--json'], - capture_output=True, text=True).stdout) -for m in data['monitors']: - if m['status'] == 'ok': - subprocess.run(['ddcswitch', 'set', str(m['index']), 'HDMI1']) -``` +### Windows Shortcuts + +Create a desktop shortcut to quickly adjust settings: -📚 **See [EXAMPLES.md](EXAMPLES.md) for comprehensive automation examples** including Stream Deck, Task Scheduler, Python, Node.js, Rust, and more. +**Target:** `C:\Path\To\ddcswitch.exe set 0 HDMI1` + +📚 **For more examples** including hotkeys, automation scripts, Stream Deck integration, and advanced usage, see **[EXAMPLES.md](EXAMPLES.md)**. ## Troubleshooting @@ -389,31 +252,17 @@ for m in data['monitors']: - DDC/CI can be slow - wait a few seconds between commands - Some monitors need to be on the target input at least once before DDC/CI can switch to it - Check monitor OSD settings for DDC/CI enable/disable options +- Power cycle the monitor and/or remove and reconnect the video cable ### Current input displays incorrectly Some monitors have non-standard DDC/CI implementations and may report incorrect current input values, even though input switching still works correctly. This is a monitor firmware limitation, not a tool issue. -If you need to verify DDC/CI values or troubleshoot monitor-specific issues, try [ControlMyMonitor](https://www.nirsoft.net/utils/control_my_monitor.html) by NirSoft - a comprehensive GUI tool for DDC/CI debugging. - -## Technical Details - -ddcswitch uses the Windows DXVA2 API to communicate with monitors via DDC/CI protocol. It reads/writes VCP (Virtual Control Panel) features following the MCCS specification. - -**Common VCP Codes:** -- `0x10` Brightness, `0x12` Contrast, `0x60` Input Source -- `0x01` VGA, `0x03` DVI, `0x0F` DisplayPort 1, `0x10` DisplayPort 2, `0x11` HDMI 1, `0x12` HDMI 2 - -**VCP Feature Types:** -- **Read-Write**: Can get and set values (brightness, contrast, input) -- **Read-Only**: Can only read current value (some monitor info) -- **Write-Only**: Can only set values (some calibration features) - -**NativeAOT Compatible:** Uses source generators for JSON, `DllImport` for P/Invoke, and zero reflection for reliable AOT compilation. +If you prefer a graphical interface over the command-line, try [ControlMyMonitor](https://www.nirsoft.net/utils/control_my_monitor.html) by NirSoft - a comprehensive GUI tool for DDC/CI control and debugging. ## Why Windows Only? -Linux has excellent DDC/CI support through `ddcutil`, which is more mature and feature-rich. This tool focuses on Windows where native CLI options are limited. +Linux has excellent DDC/CI support through `ddcutil`, which is more mature and feature-rich. Windows needed a similar command-line tool - while `winddcutil` exists, it requires Python dependencies. This project provides a standalone native executable with no runtime requirements, though it's not trying to be a direct clone of the Linux ddcutil. ## Contributing diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 2b507cc..0000000 --- a/build.cmd +++ /dev/null @@ -1,53 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -echo ======================================== -echo Building ddcswitch with NativeAOT -echo ======================================== -echo. - -REM Clean previous build -echo Cleaning previous build... -dotnet clean DDCSwitch\DDCSwitch.csproj -c Release -if errorlevel 1 ( - echo ERROR: Clean failed - exit /b 1 -) -echo. - -REM Build with NativeAOT -echo Building with NativeAOT... -dotnet publish DDCSwitch\DDCSwitch.csproj -c Release -r win-x64 --self-contained -if errorlevel 1 ( - echo ERROR: Build failed - exit /b 1 -) -echo. - -REM Create dist folder -echo Creating dist folder... -if not exist "dist" mkdir "dist" - -REM Copy the NativeAOT executable -echo Copying executable to dist folder... -copy /Y "DDCSwitch\bin\Release\net10.0\win-x64\publish\ddcswitch.exe" "dist\ddcswitch.exe" -if errorlevel 1 ( - echo ERROR: Failed to copy executable - exit /b 1 -) - -echo. -echo ======================================== -echo Build completed successfully! -echo Output: dist\ddcswitch.exe -echo ======================================== - -REM Display file size -for %%A in ("dist\ddcswitch.exe") do ( - set size=%%~zA - set /a sizeMB=!size! / 1048576 - echo File size: !sizeMB! MB -) - -endlocal - diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..ae4d5f1 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,47 @@ +Write-Host "========================================" +Write-Host "Building ddcswitch with NativeAOT" +Write-Host "========================================" +Write-Host "" + +# Clean previous build +Write-Host "Cleaning previous build..." +dotnet clean DDCSwitch\DDCSwitch.csproj -c Release +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Clean failed" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Build with NativeAOT +Write-Host "Building with NativeAOT..." +dotnet publish DDCSwitch\DDCSwitch.csproj -c Release -r win-x64 --self-contained +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Build failed" -ForegroundColor Red + exit 1 +} +Write-Host "" + +# Create dist folder +Write-Host "Creating dist folder..." +if (-not (Test-Path "dist")) { + New-Item -ItemType Directory -Path "dist" | Out-Null +} + +# Copy the NativeAOT executable +Write-Host "Copying executable to dist folder..." +Copy-Item -Path "DDCSwitch\bin\Release\net10.0\win-x64\publish\ddcswitch.exe" -Destination "dist\ddcswitch.exe" -Force +if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to copy executable" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "========================================" +Write-Host "Build completed successfully!" +Write-Host "Output: dist\ddcswitch.exe" +Write-Host "========================================" + +# Display file size +$fileInfo = Get-Item "dist\ddcswitch.exe" +$sizeMB = [math]::Round($fileInfo.Length / 1MB, 2) +Write-Host "File size: $sizeMB MB" From b7f0168ec5418ad87e7bf5c44fb8b9c1be712336 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:17:53 -0600 Subject: [PATCH 16/17] Enhance monitor name resolution with DDC/CI priority and improve EDID data retrieval --- DDCSwitch/Commands/ConsoleOutputFormatter.cs | 2 +- DDCSwitch/Commands/InfoCommand.cs | 4 +- DDCSwitch/Commands/ListCommand.cs | 4 +- DDCSwitch/DdcCiMonitorIdentifier.cs | 326 +++++++++++++ DDCSwitch/EdidParser.cs | 465 ++++++++++++++++++ DDCSwitch/HardwareInspector.cs | 483 +++++++++++++++++++ DDCSwitch/JsonContext.cs | 10 + DDCSwitch/Monitor.cs | 159 +++++- DDCSwitch/MonitorNameResolver.cs | 203 ++++++++ DDCSwitch/NativeMethods.cs | 368 +++++++++++++- DDCSwitch/VcpAnalyzer.cs | 389 +++++++++++++++ README.md | 10 + 12 files changed, 2401 insertions(+), 22 deletions(-) create mode 100644 DDCSwitch/DdcCiMonitorIdentifier.cs create mode 100644 DDCSwitch/HardwareInspector.cs create mode 100644 DDCSwitch/MonitorNameResolver.cs create mode 100644 DDCSwitch/VcpAnalyzer.cs diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs index 2afe711..0784434 100644 --- a/DDCSwitch/Commands/ConsoleOutputFormatter.cs +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -43,7 +43,7 @@ public static void WriteMonitorDetails(Monitor monitor) { // Header with monitor identification var headerPanel = new Panel( - $"[bold white]{monitor.Name}[/]\n" + + $"[bold white]{monitor.ResolvedName}[/]\n" + $"[dim]Device:[/] [cyan]{monitor.DeviceName}[/] " + $"[dim]Primary:[/] {(monitor.IsPrimary ? "[green]Yes[/]" : "[dim]No[/]")}") { diff --git a/DDCSwitch/Commands/InfoCommand.cs b/DDCSwitch/Commands/InfoCommand.cs index 79fbcd8..58399df 100644 --- a/DDCSwitch/Commands/InfoCommand.cs +++ b/DDCSwitch/Commands/InfoCommand.cs @@ -87,7 +87,7 @@ private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) { var monitorRef = new MonitorReference( monitor.Index, - monitor.Name, + monitor.ResolvedName, monitor.DeviceName, monitor.IsPrimary); @@ -166,7 +166,7 @@ private static int DisplayMonitorInfo(Monitor monitor, bool jsonOutput) { if (jsonOutput) { - var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var monitorRef = new MonitorReference(monitor.Index, monitor.ResolvedName, monitor.DeviceName, monitor.IsPrimary); var error = new ErrorResponse(false, $"Failed to retrieve monitor information: {ex.Message}", monitorRef); Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); } diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs index db5b439..be75f62 100644 --- a/DDCSwitch/Commands/ListCommand.cs +++ b/DDCSwitch/Commands/ListCommand.cs @@ -118,7 +118,7 @@ private static void OutputJsonList(List monitors, bool verboseOutput) return new MonitorInfo( monitor.Index, - monitor.Name, + monitor.ResolvedName, monitor.DeviceName, monitor.IsPrimary, inputName, @@ -254,7 +254,7 @@ private static void OutputTableList(List monitors, bool verboseOutput) var row = new List { monitor.IsPrimary ? $"[bold cyan]{monitor.Index}[/] [yellow]●[/]" : $"[cyan]{monitor.Index}[/]", - monitor.Name, + monitor.ResolvedName, $"[dim]{monitor.DeviceName}[/]", inputInfo }; diff --git a/DDCSwitch/DdcCiMonitorIdentifier.cs b/DDCSwitch/DdcCiMonitorIdentifier.cs new file mode 100644 index 0000000..bb16f97 --- /dev/null +++ b/DDCSwitch/DdcCiMonitorIdentifier.cs @@ -0,0 +1,326 @@ +using System.Text; + +namespace DDCSwitch; + +/// +/// Information about monitor identity obtained via DDC/CI +/// +public record MonitorIdentityInfo( + string? ControllerManufacturer, + string? FirmwareLevel, + string? ControllerVersion, + string? CapabilitiesString, + Dictionary IdentificationVcpValues, + bool IsFromDdcCi +); + +/// +/// Provides DDC/CI-based monitor identification functionality +/// +public static class DdcCiMonitorIdentifier +{ + // VCP codes for monitor identification + private const byte VCP_CONTROLLER_MANUFACTURER = 0xC4; + private const byte VCP_FIRMWARE_LEVEL = 0xC2; + private const byte VCP_CONTROLLER_VERSION = 0xC8; + private const byte VCP_CAPABILITIES_REQUEST = 0xF3; + + /// + /// Attempts to get monitor identity information via DDC/CI + /// + /// Monitor to query + /// Monitor identity information or null if DDC/CI is not available + public static MonitorIdentityInfo? GetIdentityViaDdcCi(Monitor monitor) + { + if (monitor == null) + { + return null; + } + + var identificationValues = new Dictionary(); + + // Try to get controller manufacturer + string? controllerManufacturer = GetControllerManufacturer(monitor); + if (controllerManufacturer != null && monitor.TryGetVcpFeature(VCP_CONTROLLER_MANUFACTURER, out uint mfgValue, out _)) + { + identificationValues[VCP_CONTROLLER_MANUFACTURER] = mfgValue; + } + + // Try to get firmware level + string? firmwareLevel = GetFirmwareLevel(monitor); + if (firmwareLevel != null && monitor.TryGetVcpFeature(VCP_FIRMWARE_LEVEL, out uint fwValue, out _)) + { + identificationValues[VCP_FIRMWARE_LEVEL] = fwValue; + } + + // Try to get controller version + string? controllerVersion = GetControllerVersion(monitor); + if (controllerVersion != null && monitor.TryGetVcpFeature(VCP_CONTROLLER_VERSION, out uint verValue, out _)) + { + identificationValues[VCP_CONTROLLER_VERSION] = verValue; + } + + // Try to get capabilities string + string? capabilitiesString = GetCapabilitiesString(monitor); + + // If we got any DDC/CI data, return the identity info + if (controllerManufacturer != null || firmwareLevel != null || + controllerVersion != null || capabilitiesString != null) + { + return new MonitorIdentityInfo( + controllerManufacturer, + firmwareLevel, + controllerVersion, + capabilitiesString, + identificationValues, + true + ); + } + + return null; + } + + /// + /// Attempts to retrieve DDC/CI capabilities string from the monitor + /// + /// Monitor to query + /// Capabilities string or null if not available + public static string? GetCapabilitiesString(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + // DDC/CI capabilities string retrieval is complex and requires special handling + // For now, we'll attempt to read the capabilities request VCP code + // In a full implementation, this would use GetCapabilitiesString DDC/CI command + + // Try to read capabilities request VCP code as a fallback + if (monitor.TryGetVcpFeature(VCP_CAPABILITIES_REQUEST, out uint capValue, out uint maxValue)) + { + // This is a simplified approach - real capabilities string retrieval + // would require implementing the full DDC/CI capabilities protocol + return $"(caps,mccs_ver(2.1),vcp({capValue:X2}))"; + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Attempts to get controller manufacturer information via VCP code 0xC4 + /// + /// Monitor to query + /// Controller manufacturer string or null if not available + public static string? GetControllerManufacturer(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + if (monitor.TryGetVcpFeature(VCP_CONTROLLER_MANUFACTURER, out uint value, out _)) + { + // Convert the value to a manufacturer string + // The format varies by manufacturer, but often uses ASCII encoding + return DecodeManufacturerValue(value); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Attempts to get firmware level information via VCP code 0xC2 + /// + /// Monitor to query + /// Firmware level string or null if not available + public static string? GetFirmwareLevel(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + if (monitor.TryGetVcpFeature(VCP_FIRMWARE_LEVEL, out uint value, out _)) + { + // Firmware level is typically encoded as a version number + // Format varies by manufacturer but often uses BCD or simple numeric encoding + return DecodeFirmwareLevel(value); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Attempts to get controller version information via VCP code 0xC8 + /// + /// Monitor to query + /// Controller version string or null if not available + public static string? GetControllerVersion(Monitor monitor) + { + if (monitor == null) + return null; + + try + { + if (monitor.TryGetVcpFeature(VCP_CONTROLLER_VERSION, out uint value, out _)) + { + // Controller version is typically encoded as a version number + return DecodeControllerVersion(value); + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Decodes manufacturer value from VCP code 0xC4 + /// + private static string? DecodeManufacturerValue(uint value) + { + if (value == 0) + return null; + + try + { + // Try to decode as ASCII characters (common format) + var bytes = new List(); + + // Extract bytes from the 32-bit value + for (int i = 0; i < 4; i++) + { + byte b = (byte)((value >> (i * 8)) & 0xFF); + if (b != 0 && b >= 0x20 && b <= 0x7E) // Printable ASCII + { + bytes.Add(b); + } + } + + if (bytes.Count > 0) + { + bytes.Reverse(); // Most significant byte first + return Encoding.ASCII.GetString(bytes.ToArray()).Trim(); + } + + // If ASCII decoding fails, return as hex value + return $"0x{value:X8}"; + } + catch + { + return $"0x{value:X8}"; + } + } + + /// + /// Decodes firmware level from VCP code 0xC2 + /// + private static string? DecodeFirmwareLevel(uint value) + { + if (value == 0) + return null; + + try + { + // Common formats: + // - BCD encoding: 0x0123 = version 1.23 + // - Simple numeric: 0x001A = version 26 + // - ASCII encoding: similar to manufacturer + + // Try BCD decoding first (most common) + if (value <= 0xFFFF) + { + uint major = (value >> 8) & 0xFF; + uint minor = value & 0xFF; + + // Check if it looks like BCD + if ((major & 0xF0) <= 0x90 && (major & 0x0F) <= 0x09 && + (minor & 0xF0) <= 0x90 && (minor & 0x0F) <= 0x09) + { + uint majorBcd = ((major >> 4) * 10) + (major & 0x0F); + uint minorBcd = ((minor >> 4) * 10) + (minor & 0x0F); + return $"{majorBcd}.{minorBcd:D2}"; + } + + // Try simple major.minor format + if (major > 0 && major <= 99 && minor <= 99) + { + return $"{major}.{minor:D2}"; + } + } + + // Try ASCII decoding + var asciiResult = DecodeManufacturerValue(value); + if (asciiResult != null && !asciiResult.StartsWith("0x")) + { + return asciiResult; + } + + // Fallback to hex representation + return $"0x{value:X4}"; + } + catch + { + return $"0x{value:X4}"; + } + } + + /// + /// Decodes controller version from VCP code 0xC8 + /// + private static string? DecodeControllerVersion(uint value) + { + if (value == 0) + return null; + + try + { + // Similar to firmware level but may have different encoding + // Some monitors use this for hardware revision + + if (value <= 0xFFFF) + { + uint high = (value >> 8) & 0xFF; + uint low = value & 0xFF; + + // Try simple version format + if (high > 0 && high <= 99) + { + return $"{high}.{low}"; + } + } + + // Try ASCII decoding + var asciiResult = DecodeManufacturerValue(value); + if (asciiResult != null && !asciiResult.StartsWith("0x")) + { + return asciiResult; + } + + // Fallback to hex representation + return $"0x{value:X4}"; + } + catch + { + return $"0x{value:X4}"; + } + } +} \ No newline at end of file diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/EdidParser.cs index 00b182c..72b2949 100644 --- a/DDCSwitch/EdidParser.cs +++ b/DDCSwitch/EdidParser.cs @@ -1,4 +1,5 @@ using System.Text; +using Microsoft.Win32; namespace DDCSwitch; @@ -64,11 +65,475 @@ public record ChromaticityCoordinates( ColorPoint Blue, ColorPoint White); +/// +/// Represents a registry EDID entry with metadata. +/// +/// Full registry path to the EDID entry +/// Raw EDID byte data +/// Last modification time of the registry entry +/// Whether this entry corresponds to an active monitor +public record RegistryEdidEntry( + string RegistryPath, + byte[] EdidData, + DateTime LastWriteTime, + bool IsCurrentlyActive +); + +/// +/// Represents complete EDID information with metadata from enhanced parsing. +/// +/// Full manufacturer name +/// 3-letter manufacturer code +/// Product code from EDID +/// Serial number from EDID +/// Week of manufacture (1-53) +/// Year of manufacture +/// EDID version number +/// EDID revision number +/// Video input definition string +/// Color space information +/// Raw EDID byte data +/// Whether this EDID came from an active registry entry +public record ParsedEdidInfo( + string ManufacturerName, + string ManufacturerCode, + ushort ProductCode, + uint SerialNumber, + int ManufactureWeek, + int ManufactureYear, + byte EdidVersion, + byte EdidRevision, + string VideoInputDefinition, + EdidColorInfo ColorInfo, + byte[] RawData, + bool IsFromActiveEntry +); + +/// +/// Represents color space information from EDID. +/// +/// White point X coordinate +/// White point Y coordinate +/// Red X coordinate +/// Red Y coordinate +/// Green X coordinate +/// Green Y coordinate +/// Blue X coordinate +/// Blue Y coordinate +public record EdidColorInfo( + float WhitePointX, + float WhitePointY, + float RedX, + float RedY, + float GreenX, + float GreenY, + float BlueX, + float BlueY +); + /// /// Parses EDID (Extended Display Identification Data) blocks to extract monitor information. +/// Enhanced with registry conflict resolution and active entry detection. /// public static class EdidParser { + /// + /// Parses EDID information from registry with enhanced conflict resolution. + /// + /// Device name from MONITORINFOEX (e.g., \\.\DISPLAY1) + /// Complete EDID information or null if not found + public static ParsedEdidInfo? ParseFromRegistry(string deviceName) + { + var edidData = NativeMethods.GetEdidFromRegistry(deviceName); + if (edidData == null || edidData.Length < 128) + return null; + + return ParseFromBytes(edidData, false); + } + + /// + /// Parses EDID information from active registry entry using physical monitor handle. + /// + /// Device name from MONITORINFOEX + /// Physical monitor handle for cross-referencing + /// Complete EDID information from active entry or null if not found + public static ParsedEdidInfo? ParseFromActiveRegistry(string deviceName, IntPtr physicalMonitorHandle) + { + try + { + // Extract hardware ID from device name for registry lookup + string? hardwareId = ExtractHardwareIdFromDeviceName(deviceName); + if (hardwareId == null) + return null; + + // Find the active registry entry for this hardware ID + var activeEntry = FindActiveRegistryEntry(hardwareId, physicalMonitorHandle); + + return activeEntry == null ? null : ParseFromBytes(activeEntry.EdidData, true); + } + catch + { + return null; + } + } + + /// + /// Parses EDID information from raw byte data. + /// + /// Raw EDID byte array + /// Whether this data came from an active registry entry + /// Complete EDID information or null if invalid + public static ParsedEdidInfo? ParseFromBytes(byte[] edidData, bool isFromActiveEntry = false) + { + if (edidData == null || edidData.Length < 128 || !ValidateHeader(edidData)) + return null; + + try + { + var manufacturerCode = ParseManufacturerId(edidData) ?? "UNK"; + var manufacturerName = GetManufacturerName(manufacturerCode); + var productCode = ParseProductCode(edidData) ?? 0; + var serialNumber = ParseNumericSerialNumber(edidData) ?? 0; + var manufactureWeek = ParseManufactureWeek(edidData) ?? 0; + var manufactureYear = ParseManufactureYear(edidData) ?? 0; + var edidVersion = ParseEdidVersion(edidData); + var videoInput = ParseVideoInputDefinition(edidData); + var chromaticity = ParseChromaticity(edidData); + + var colorInfo = new EdidColorInfo( + (float)(chromaticity?.White.X ?? 0.0), + (float)(chromaticity?.White.Y ?? 0.0), + (float)(chromaticity?.Red.X ?? 0.0), + (float)(chromaticity?.Red.Y ?? 0.0), + (float)(chromaticity?.Green.X ?? 0.0), + (float)(chromaticity?.Green.Y ?? 0.0), + (float)(chromaticity?.Blue.X ?? 0.0), + (float)(chromaticity?.Blue.Y ?? 0.0) + ); + + return new ParsedEdidInfo( + manufacturerName, + manufacturerCode, + productCode, + serialNumber, + manufactureWeek, + manufactureYear, + edidVersion?.Major ?? 0, + edidVersion?.Minor ?? 0, + videoInput?.ToString() ?? "Unknown", + colorInfo, + edidData, + isFromActiveEntry + ); + } + catch + { + return null; + } + } + + /// + /// Finds all registry entries for a given hardware ID. + /// + /// Hardware ID to search for (format: ManufacturerKey\InstanceKey) + /// List of all registry entries found for this hardware ID + public static List FindAllRegistryEntries(string hardwareId) + { + var entries = new List(); + + try + { + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return entries; + + // Parse hardware ID (format: ManufacturerKey\InstanceKey) + var parts = hardwareId.Split('\\'); + if (parts.Length != 2) return entries; + + string mfgKey = parts[0]; + string targetInstanceKey = parts[1]; + + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) return entries; + + // Look for the specific instance and similar instances (for conflict detection) + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + // Include exact match and similar instances (same base hardware ID) + if (instanceKey == targetInstanceKey || + instanceKey.StartsWith(targetInstanceKey.Split('&')[0], StringComparison.OrdinalIgnoreCase)) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + // Read EDID data + if (deviceParams.GetValue("EDID") is not byte[] edidData || edidData.Length < 128) + continue; + + // Validate EDID header + if (!ValidateHeader(edidData)) + continue; + + // Get registry entry metadata + var registryPath = $@"{displayKey}\{mfgKey}\{instanceKey}"; + var lastWriteTime = GetRegistryKeyLastWriteTime(instanceSubKey) ?? DateTime.MinValue; + + entries.Add(new RegistryEdidEntry( + registryPath, + edidData, + lastWriteTime, + instanceKey == targetInstanceKey // Mark as active if exact match + )); + } + } + } + catch + { + // Return partial results on error + } + + return entries; + } + + /// + /// Finds the active registry entry for a hardware ID using physical monitor handle. + /// + /// Hardware ID to search for + /// Physical monitor handle for cross-referencing + /// Active registry entry or null if not found + public static RegistryEdidEntry? FindActiveRegistryEntry(string hardwareId, IntPtr physicalMonitorHandle) + { + var allEntries = FindAllRegistryEntries(hardwareId); + if (allEntries.Count == 0) + return null; + + // If only one entry, assume it's active + if (allEntries.Count == 1) + { + var entry = allEntries[0]; + return entry with { IsCurrentlyActive = true }; + } + + // Multiple entries - use heuristics to determine active one + var activeEntry = ResolveRegistryConflicts(allEntries, physicalMonitorHandle); + if (activeEntry != null) + { + return activeEntry with { IsCurrentlyActive = true }; + } + + // Fallback: return most recently modified entry + var mostRecent = allEntries.OrderByDescending(e => e.LastWriteTime).First(); + return mostRecent with { IsCurrentlyActive = true }; + } + + /// + /// Resolves conflicts when multiple registry entries exist for the same hardware ID. + /// + /// List of conflicting registry entries + /// Physical monitor handle for cross-referencing + /// The most likely active entry or null if cannot be determined + private static RegistryEdidEntry? ResolveRegistryConflicts(List entries, IntPtr physicalMonitorHandle) + { + if (entries.Count <= 1) + return entries.FirstOrDefault(); + + // Strategy 1: Use physical monitor handle to cross-reference with registry + // This is a simplified approach - in a full implementation, we would use + // Windows APIs to map physical monitor handles to registry entries + + // Strategy 2: Prefer entries with more recent timestamps + var recentEntries = entries + .Where(e => e.LastWriteTime > DateTime.Now.AddDays(-30)) // Within last 30 days + .OrderByDescending(e => e.LastWriteTime) + .ToList(); + + if (recentEntries.Count > 0) + return recentEntries.First(); + + // Strategy 3: Prefer entries with valid EDID data and complete information + var validEntries = entries + .Where(e => HasCompleteEdidInfo(e.EdidData)) + .OrderByDescending(e => e.LastWriteTime) + .ToList(); + + if (validEntries.Count > 0) + return validEntries.First(); + + // Fallback: return most recent entry + return entries.OrderByDescending(e => e.LastWriteTime).FirstOrDefault(); + } + + /// + /// Checks if EDID data contains complete information. + /// + /// EDID byte array + /// True if EDID contains manufacturer, product, and other key information + private static bool HasCompleteEdidInfo(byte[] edidData) + { + if (edidData == null || edidData.Length < 128) + return false; + + var manufacturerId = ParseManufacturerId(edidData); + var productCode = ParseProductCode(edidData); + var modelName = ParseModelName(edidData); + + return !string.IsNullOrEmpty(manufacturerId) && + productCode.HasValue && + productCode.Value != 0 && + (!string.IsNullOrEmpty(modelName) || ParseNumericSerialNumber(edidData).HasValue); + } + + /// + /// Extracts hardware ID from Windows device name using proper Windows APIs. + /// + /// Device name (e.g., \\.\DISPLAY1) + /// Hardware ID or null if cannot be extracted + private static string? ExtractHardwareIdFromDeviceName(string deviceName) + { + try + { + // For now, we'll use a more sophisticated approach to map device names to hardware IDs + // This involves checking which registry entries correspond to currently active displays + + // Extract display number from device name (e.g., \\.\DISPLAY1 -> 1) + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (!int.TryParse(displayNum, out int displayIndex)) + return null; + + // Get all currently active display devices and their registry paths + var activeDisplays = GetActiveDisplayDevices(); + + // Find the hardware ID for the display at the given index + if (displayIndex > 0 && displayIndex <= activeDisplays.Count) + { + return activeDisplays[displayIndex - 1]; // Convert to 0-based index + } + + return null; + } + catch + { + return null; + } + } + + /// + /// Gets hardware IDs for all currently active display devices. + /// + /// List of hardware IDs for active displays + private static List GetActiveDisplayDevices() + { + var activeDisplays = new List(); + + try + { + // This is a simplified implementation. In a full implementation, we would use + // EnumDisplayDevices and SetupDi APIs to get the actual hardware IDs. + // For now, we'll use a heuristic based on registry timestamps and validation. + + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return activeDisplays; + + var candidateEntries = new List<(string hardwareId, DateTime lastWrite, byte[] edid)>(); + + // Collect all valid EDID entries with their metadata + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + var edidData = deviceParams.GetValue("EDID") as byte[]; + if (edidData == null || edidData.Length < 128 || !ValidateHeader(edidData)) + continue; + + // Check if this entry has recent activity (heuristic for active device) + var lastWrite = GetRegistryKeyLastWriteTime(instanceSubKey) ?? DateTime.MinValue; + + // Use a combination of manufacturer key and instance as hardware ID + string hardwareId = $"{mfgKey}\\{instanceKey}"; + + candidateEntries.Add((hardwareId, lastWrite, edidData)); + } + } + + // Sort by last write time (most recent first) and take the most recent entries + // This heuristic assumes that recently modified registry entries correspond to active monitors + var sortedEntries = candidateEntries + .OrderByDescending(e => e.lastWrite) + .Take(10) // Reasonable limit for number of monitors + .ToList(); + + // Further filter by checking for unique EDID signatures + var uniqueEdids = new HashSet(); + foreach (var entry in sortedEntries) + { + // Create a signature from manufacturer ID and product code + var manufacturerId = ParseManufacturerId(entry.edid); + var productCode = ParseProductCode(entry.edid); + var signature = $"{manufacturerId}_{productCode:X4}"; + + if (!uniqueEdids.Contains(signature)) + { + uniqueEdids.Add(signature); + activeDisplays.Add(entry.hardwareId); + } + } + + return activeDisplays; + } + catch + { + return activeDisplays; + } + } + + /// + /// Gets the last write time of a registry key using heuristics. + /// + /// Registry key + /// Last write time or null if cannot be determined + private static DateTime? GetRegistryKeyLastWriteTime(RegistryKey key) + { + try + { + // Since we can't directly get registry key timestamps in .NET without P/Invoke, + // we'll use a heuristic based on the registry structure and common patterns + + // Check if there are any subkeys with timestamps we can infer from + var subKeyNames = key.GetSubKeyNames(); + var valueNames = key.GetValueNames(); + + // If the key has many subkeys or values, it's likely more recent + // This is a rough heuristic - in practice, we'd use RegQueryInfoKey API + + if (subKeyNames.Length > 0 || valueNames.Length > 0) + { + // Assume recent activity if the key has content + return DateTime.Now.AddDays(-1); // Assume modified within last day + } + + // Fallback to a reasonable default + return DateTime.Now.AddDays(-30); + } + catch + { + return DateTime.Now.AddDays(-30); + } + } + /// /// Parses manufacturer ID from EDID bytes. /// diff --git a/DDCSwitch/HardwareInspector.cs b/DDCSwitch/HardwareInspector.cs new file mode 100644 index 0000000..a8e59dd --- /dev/null +++ b/DDCSwitch/HardwareInspector.cs @@ -0,0 +1,483 @@ +using Microsoft.Win32; +using System.Runtime.InteropServices; + +namespace DDCSwitch; + +/// +/// Connection type for monitor hardware +/// +public enum ConnectionType +{ + Unknown, + HDMI, + DisplayPort, + DVI, + VGA, + USBC, + eDP, + LVDS +} + +/// +/// Hardware information for a monitor +/// +public record HardwareInfo( + string GraphicsDriver, + ConnectionType ConnectionType, + string HardwarePath, + bool IsEmbeddedDisplay, + DdcCiStatus DdcCiStatus +); + +/// +/// Provides hardware inspection functionality for monitors +/// +public static class HardwareInspector +{ + /// + /// Inspects monitor hardware and returns comprehensive hardware information + /// + /// Monitor to inspect + /// Device name from Windows (e.g., \\.\DISPLAY1) + /// Hardware information for the monitor + public static HardwareInfo InspectMonitor(Monitor monitor, string deviceName) + { + if (monitor == null || string.IsNullOrEmpty(deviceName)) + { + return new HardwareInfo( + "Unknown", + ConnectionType.Unknown, + "Unknown", + false, + DdcCiStatus.Unknown + ); + } + + try + { + // Get graphics driver information + string graphicsDriver = GetGraphicsDriver(deviceName); + + // Determine connection type + ConnectionType connectionType = DetermineConnectionType(deviceName); + + // Get hardware path information + string hardwarePath = GetHardwarePath(deviceName); + + // Check if this is an embedded display + bool isEmbeddedDisplay = IsEmbeddedDisplay(connectionType, deviceName); + + // Assess DDC/CI responsiveness status + DdcCiStatus ddcCiStatus = AssessDdcCiStatus(monitor); + + return new HardwareInfo( + graphicsDriver, + connectionType, + hardwarePath, + isEmbeddedDisplay, + ddcCiStatus + ); + } + catch (Exception) + { + // Graceful degradation on any error + return new HardwareInfo( + "Unknown", + ConnectionType.Unknown, + deviceName, + false, + DdcCiStatus.Unknown + ); + } + } + + /// + /// Determines the connection type for a monitor + /// + /// Device name from Windows + /// Connection type + public static ConnectionType DetermineConnectionType(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return ConnectionType.Unknown; + + try + { + // Try to get connection information from WMI + var connectionType = GetConnectionTypeFromWmi(deviceName); + if (connectionType != ConnectionType.Unknown) + return connectionType; + + // Try to get connection information from registry + connectionType = GetConnectionTypeFromRegistry(deviceName); + if (connectionType != ConnectionType.Unknown) + return connectionType; + + // Try to infer from device name patterns + return InferConnectionTypeFromDeviceName(deviceName); + } + catch + { + return ConnectionType.Unknown; + } + } + + /// + /// Gets the graphics driver information for a monitor + /// + /// Device name from Windows + /// Graphics driver name and version + public static string GetGraphicsDriver(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return "Unknown"; + + try + { + // Try to get driver information from WMI + var driverInfo = GetDriverInfoFromWmi(deviceName); + if (!string.IsNullOrEmpty(driverInfo)) + return driverInfo; + + // Try to get driver information from registry + driverInfo = GetDriverInfoFromRegistry(deviceName); + if (!string.IsNullOrEmpty(driverInfo)) + return driverInfo; + + return "Unknown"; + } + catch + { + return "Unknown"; + } + } + + /// + /// Gets Windows hardware path information for a monitor + /// + /// Device name from Windows + /// Hardware path string + private static string GetHardwarePath(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return "Unknown"; + + try + { + // Try to get hardware path from WMI + var hardwarePath = GetHardwarePathFromWmi(deviceName); + if (!string.IsNullOrEmpty(hardwarePath)) + return hardwarePath; + + // Fallback to device name + return deviceName; + } + catch + { + return deviceName; + } + } + + /// + /// Determines if the display is embedded (laptop screen) + /// + /// Connection type + /// Device name + /// True if embedded display + private static bool IsEmbeddedDisplay(ConnectionType connectionType, string deviceName) + { + // eDP and LVDS are typically embedded displays + if (connectionType == ConnectionType.eDP || connectionType == ConnectionType.LVDS) + return true; + + try + { + // Check for laptop indicators in WMI + return CheckForEmbeddedDisplayInWmi(deviceName); + } + catch + { + return false; + } + } + + /// + /// Assesses DDC/CI responsiveness status for the monitor + /// + /// Monitor to assess + /// DDC/CI status + private static DdcCiStatus AssessDdcCiStatus(Monitor monitor) + { + if (monitor == null) + return DdcCiStatus.Unknown; + + try + { + // Use existing VCP analyzer to determine DDC/CI status + return VcpAnalyzer.TestDdcCiComprehensive(monitor); + } + catch + { + return DdcCiStatus.Unknown; + } + } + + /// + /// Gets connection type from WMI + /// + private static ConnectionType GetConnectionTypeFromWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return GetConnectionTypeFromRegistry(deviceName); + } + + /// + /// Gets connection type from registry + /// + private static ConnectionType GetConnectionTypeFromRegistry(string deviceName) + { + try + { + // Look in display registry for connection information + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return ConnectionType.Unknown; + + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + // Check hardware ID for connection type indicators + var hardwareId = instanceSubKey.GetValue("HardwareID") as string[]; + if (hardwareId != null) + { + foreach (var id in hardwareId) + { + var idUpper = id.ToUpperInvariant(); + + if (idUpper.Contains("HDMI")) + return ConnectionType.HDMI; + if (idUpper.Contains("DISPLAYPORT") || idUpper.Contains("DP")) + return ConnectionType.DisplayPort; + if (idUpper.Contains("DVI")) + return ConnectionType.DVI; + if (idUpper.Contains("VGA")) + return ConnectionType.VGA; + if (idUpper.Contains("EDP")) + return ConnectionType.eDP; + if (idUpper.Contains("LVDS")) + return ConnectionType.LVDS; + } + } + } + } + } + catch + { + // Registry access error + } + + return ConnectionType.Unknown; + } + + /// + /// Infers connection type from device name patterns + /// + private static ConnectionType InferConnectionTypeFromDeviceName(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return ConnectionType.Unknown; + + var nameUpper = deviceName.ToUpperInvariant(); + + // Check for common patterns in device names + if (nameUpper.Contains("HDMI")) + return ConnectionType.HDMI; + if (nameUpper.Contains("DP") || nameUpper.Contains("DISPLAYPORT")) + return ConnectionType.DisplayPort; + if (nameUpper.Contains("DVI")) + return ConnectionType.DVI; + if (nameUpper.Contains("VGA")) + return ConnectionType.VGA; + if (nameUpper.Contains("EDP")) + return ConnectionType.eDP; + if (nameUpper.Contains("LVDS")) + return ConnectionType.LVDS; + + return ConnectionType.Unknown; + } + + /// + /// Gets driver information from WMI + /// + private static string GetDriverInfoFromWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return GetDriverInfoFromRegistry(deviceName); + } + + /// + /// Gets driver information from registry + /// + private static string GetDriverInfoFromRegistry(string deviceName) + { + try + { + // Look for video controller information in registry + const string videoKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}"; + using var videoRoot = Registry.LocalMachine.OpenSubKey(videoKey); + if (videoRoot == null) return string.Empty; + + foreach (string subKeyName in videoRoot.GetSubKeyNames()) + { + if (subKeyName == "Properties") continue; + + using var subKey = videoRoot.OpenSubKey(subKeyName); + if (subKey == null) continue; + + var driverDesc = subKey.GetValue("DriverDesc")?.ToString(); + var driverVersion = subKey.GetValue("DriverVersion")?.ToString(); + + if (!string.IsNullOrEmpty(driverDesc)) + { + var result = driverDesc; + if (!string.IsNullOrEmpty(driverVersion)) + { + result += $" (v{driverVersion})"; + } + return result; + } + } + } + catch + { + // Registry access error + } + + return string.Empty; + } + + /// + /// Gets hardware path from registry + /// + private static string GetHardwarePathFromRegistry(string deviceName) + { + try + { + // Look in display registry for hardware path information + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return string.Empty; + + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + // Get hardware ID as the hardware path + var hardwareId = instanceSubKey.GetValue("HardwareID") as string[]; + if (hardwareId != null && hardwareId.Length > 0) + { + return hardwareId[0]; + } + + // Fallback to device instance path + var deviceInstancePath = $@"DISPLAY\{mfgKey}\{instanceKey}"; + return deviceInstancePath; + } + } + } + catch + { + // Registry access error + } + + return string.Empty; + } + + /// + /// Checks for embedded display indicators in registry + /// + private static bool CheckForEmbeddedDisplayInRegistry(string deviceName) + { + try + { + // Check for laptop indicators in system information + const string systemKey = @"HARDWARE\DESCRIPTION\System"; + using var systemRoot = Registry.LocalMachine.OpenSubKey(systemKey); + if (systemRoot != null) + { + var systemBiosVersion = systemRoot.GetValue("SystemBiosVersion") as string[]; + if (systemBiosVersion != null) + { + foreach (var version in systemBiosVersion) + { + var versionUpper = version.ToUpperInvariant(); + if (versionUpper.Contains("LAPTOP") || versionUpper.Contains("PORTABLE") || + versionUpper.Contains("NOTEBOOK") || versionUpper.Contains("MOBILE")) + { + return true; + } + } + } + } + + // Check for battery presence in registry (indicates laptop) + const string batteryKey = @"SYSTEM\CurrentControlSet\Services\battery"; + using var batteryRoot = Registry.LocalMachine.OpenSubKey(batteryKey); + if (batteryRoot != null) + { + return true; + } + + // Check for ACPI battery devices + const string acpiKey = @"SYSTEM\CurrentControlSet\Enum\ACPI"; + using var acpiRoot = Registry.LocalMachine.OpenSubKey(acpiKey); + if (acpiRoot != null) + { + foreach (string deviceKey in acpiRoot.GetSubKeyNames()) + { + if (deviceKey.StartsWith("PNP0C0A", StringComparison.OrdinalIgnoreCase)) // Battery device + { + return true; + } + } + } + } + catch + { + // Registry access error + } + + return false; + } + + /// + /// Gets hardware path from WMI + /// + private static string GetHardwarePathFromWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return GetHardwarePathFromRegistry(deviceName); + } + + /// + /// Checks for embedded display indicators in WMI + /// + private static bool CheckForEmbeddedDisplayInWmi(string deviceName) + { + // WMI is not compatible with NativeAOT, use registry-based approach instead + return CheckForEmbeddedDisplayInRegistry(deviceName); + } +} \ No newline at end of file diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index 9ea4b3b..b40569a 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -14,9 +14,19 @@ namespace DDCSwitch; [JsonSerializable(typeof(MonitorReference))] [JsonSerializable(typeof(MonitorInfoResponse))] [JsonSerializable(typeof(EdidInfo))] +[JsonSerializable(typeof(ParsedEdidInfo))] +[JsonSerializable(typeof(RegistryEdidEntry))] +[JsonSerializable(typeof(EdidColorInfo))] [JsonSerializable(typeof(FeaturesInfo))] [JsonSerializable(typeof(ChromaticityInfo))] [JsonSerializable(typeof(ColorPointInfo))] +[JsonSerializable(typeof(MonitorIdentityInfo))] +[JsonSerializable(typeof(VcpVersionInfo))] +[JsonSerializable(typeof(VcpTestResult))] +[JsonSerializable(typeof(VcpCapabilityInfo))] +[JsonSerializable(typeof(DdcCiStatus))] +[JsonSerializable(typeof(HardwareInfo))] +[JsonSerializable(typeof(ConnectionType))] [JsonSourceGenerationOptions( WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index 2061fa6..2c7237a 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -13,7 +13,17 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public string DeviceName { get; } = deviceName; public bool IsPrimary { get; } = isPrimary; - // EDID properties + // Enhanced name resolution with DDC/CI priority + public string ResolvedName { get; private set; } = name; // Initialize with Windows name + public bool NameFromDdcCi { get; private set; } + public bool NameFromEdid { get; private set; } + + // EDID properties (enhanced with registry conflict resolution) + public ParsedEdidInfo? ParsedEdid { get; private set; } + public List? AlternativeRegistryEntries { get; private set; } + public bool HasRegistryConflicts => AlternativeRegistryEntries?.Count > 1; + + // Legacy EDID properties (for backward compatibility) public string? ManufacturerId { get; private set; } public string? ManufacturerName { get; private set; } public string? ModelName { get; private set; } @@ -26,37 +36,154 @@ public class Monitor(int index, string name, string deviceName, bool isPrimary, public SupportedFeatures? SupportedFeatures { get; private set; } public ChromaticityCoordinates? Chromaticity { get; private set; } + // DDC/CI identification properties + public MonitorIdentityInfo? DdcCiIdentity { get; private set; } + public VcpCapabilityInfo? VcpCapabilities { get; private set; } + private IntPtr Handle { get; } = handle; private bool _disposed; /// - /// Loads EDID data from registry and populates EDID properties. + /// Loads EDID data from registry with enhanced conflict resolution and populates EDID properties. /// public void LoadEdidData() { try { - var edid = NativeMethods.GetEdidFromRegistry(DeviceName); - if (edid == null || edid.Length < 128) return; + // Use the enhanced registry method with monitor description matching first + var edidData = NativeMethods.GetEdidFromRegistryEnhanced(DeviceName, Handle, Name); + if (edidData != null && edidData.Length >= 128) + { + ParsedEdid = EdidParser.ParseFromBytes(edidData, true); + } + else + { + // Fallback to active registry parsing + ParsedEdid = EdidParser.ParseFromActiveRegistry(DeviceName, Handle); + } + + // Final fallback to legacy method + if (ParsedEdid == null) + { + ParsedEdid = EdidParser.ParseFromRegistry(DeviceName); + } + + // Load alternative registry entries for conflict detection + if (ParsedEdid != null) + { + string? hardwareId = ExtractHardwareIdFromDeviceName(DeviceName); + if (hardwareId != null) + { + AlternativeRegistryEntries = EdidParser.FindAllRegistryEntries(hardwareId); + } + } - ManufacturerId = EdidParser.ParseManufacturerId(edid); - if (ManufacturerId != null) + // Populate legacy properties for backward compatibility + if (ParsedEdid != null) { - ManufacturerName = EdidParser.GetManufacturerName(ManufacturerId); + ManufacturerId = ParsedEdid.ManufacturerCode; + ManufacturerName = ParsedEdid.ManufacturerName; + ProductCode = ParsedEdid.ProductCode; + ManufactureYear = ParsedEdid.ManufactureYear; + ManufactureWeek = ParsedEdid.ManufactureWeek; + + // Parse additional legacy properties from raw EDID data + var edid = ParsedEdid.RawData; + ModelName = EdidParser.ParseModelName(edid); + SerialNumber = EdidParser.ParseSerialNumber(edid); + EdidVersion = EdidParser.ParseEdidVersion(edid); + VideoInputDefinition = EdidParser.ParseVideoInputDefinition(edid); + SupportedFeatures = EdidParser.ParseSupportedFeatures(edid); + Chromaticity = EdidParser.ParseChromaticity(edid); } - ModelName = EdidParser.ParseModelName(edid); - SerialNumber = EdidParser.ParseSerialNumber(edid); - ProductCode = EdidParser.ParseProductCode(edid); - ManufactureYear = EdidParser.ParseManufactureYear(edid); - ManufactureWeek = EdidParser.ParseManufactureWeek(edid); - EdidVersion = EdidParser.ParseEdidVersion(edid); - VideoInputDefinition = EdidParser.ParseVideoInputDefinition(edid); - SupportedFeatures = EdidParser.ParseSupportedFeatures(edid); - Chromaticity = EdidParser.ParseChromaticity(edid); + + // Resolve monitor name with DDC/CI priority + ResolveMonitorName(); } catch { // Graceful degradation - EDID properties remain null + ResolvedName = Name; // Fallback to Windows name + } + } + + /// + /// Resolves the monitor name using DDC/CI first, then EDID, then Windows fallback. + /// + private void ResolveMonitorName() + { + try + { + // Load DDC/CI identity if not already loaded + if (DdcCiIdentity == null) + { + LoadDdcCiIdentity(); + } + + // Resolve name with priority: DDC/CI > EDID > Windows + ResolvedName = MonitorNameResolver.ResolveMonitorName(this, DdcCiIdentity, ParsedEdid); + + // Set flags to indicate name source + NameFromDdcCi = MonitorNameResolver.HasNameFromDdcCi(DdcCiIdentity); + NameFromEdid = !NameFromDdcCi && MonitorNameResolver.HasNameFromEdid(ParsedEdid); + } + catch + { + // Fallback to Windows name + ResolvedName = MonitorNameResolver.GetFallbackName(this); + NameFromDdcCi = false; + NameFromEdid = false; + } + } + + /// + /// Extracts hardware ID from Windows device name (simplified implementation). + /// + /// Device name (e.g., \\.\DISPLAY1) + /// Hardware ID or null if cannot be extracted + private static string? ExtractHardwareIdFromDeviceName(string deviceName) + { + if (string.IsNullOrEmpty(deviceName)) + return null; + + // Extract display number from device name (e.g., \\.\DISPLAY1 -> 1) + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (int.TryParse(displayNum, out int displayIndex)) + { + // Use display index as a simple hardware ID approximation + return $"DISPLAY{displayIndex}"; + } + + return null; + } + + /// + /// Loads DDC/CI identity information and populates DDC/CI properties. + /// + public void LoadDdcCiIdentity() + { + try + { + DdcCiIdentity = DdcCiMonitorIdentifier.GetIdentityViaDdcCi(this); + } + catch + { + // Graceful degradation - DDC/CI identity remains null + } + } + + /// + /// Loads VCP capability information and populates VCP properties. + /// + public void LoadVcpCapabilities() + { + try + { + VcpCapabilities = VcpAnalyzer.AnalyzeCapabilities(this); + } + catch + { + // Graceful degradation - VCP capabilities remain null } } diff --git a/DDCSwitch/MonitorNameResolver.cs b/DDCSwitch/MonitorNameResolver.cs new file mode 100644 index 0000000..186df9f --- /dev/null +++ b/DDCSwitch/MonitorNameResolver.cs @@ -0,0 +1,203 @@ +namespace DDCSwitch; + +/// +/// Resolves monitor names with DDC/CI priority over EDID registry data +/// +public static class MonitorNameResolver +{ + /// + /// Resolves the best available monitor name using DDC/CI first, then EDID, then Windows fallback + /// + /// Monitor instance + /// DDC/CI identity information (if available) + /// EDID information (if available) + /// Resolved monitor name + public static string ResolveMonitorName(Monitor monitor, MonitorIdentityInfo? ddcCiIdentity, ParsedEdidInfo? edidInfo) + { + // Priority 1: DDC/CI Controller Manufacturer + Capabilities + if (HasNameFromDdcCi(ddcCiIdentity)) + { + var ddcCiName = BuildDdcCiName(ddcCiIdentity!); + if (!string.IsNullOrEmpty(ddcCiName)) + { + return ddcCiName; + } + } + + // Priority 2: EDID Manufacturer + Model + if (HasNameFromEdid(edidInfo)) + { + var edidName = BuildEdidName(edidInfo!); + if (!string.IsNullOrEmpty(edidName)) + { + return edidName; + } + } + + // Priority 3: Windows Physical Monitor Description (fallback) + return GetFallbackName(monitor); + } + + /// + /// Checks if DDC/CI provides sufficient information for naming + /// + /// DDC/CI identity information + /// True if DDC/CI can provide a name + public static bool HasNameFromDdcCi(MonitorIdentityInfo? ddcCiIdentity) + { + return ddcCiIdentity != null && + ddcCiIdentity.IsFromDdcCi && + (!string.IsNullOrEmpty(ddcCiIdentity.ControllerManufacturer) || + !string.IsNullOrEmpty(ddcCiIdentity.CapabilitiesString)); + } + + /// + /// Checks if EDID provides sufficient information for naming + /// + /// EDID information + /// True if EDID can provide a name + public static bool HasNameFromEdid(ParsedEdidInfo? edidInfo) + { + return edidInfo != null && + (!string.IsNullOrEmpty(edidInfo.ManufacturerName) || + !string.IsNullOrEmpty(edidInfo.ManufacturerCode)); + } + + /// + /// Gets fallback name from Windows monitor description + /// + /// Monitor instance + /// Fallback name + public static string GetFallbackName(Monitor monitor) + { + if (!string.IsNullOrEmpty(monitor.Name) && monitor.Name != "Generic PnP Monitor") + { + return monitor.Name; + } + + return $"Monitor {monitor.Index}"; + } + + /// + /// Builds a monitor name from DDC/CI information + /// + /// DDC/CI identity information + /// DDC/CI-derived monitor name + private static string BuildDdcCiName(MonitorIdentityInfo ddcCiIdentity) + { + var nameParts = new List(); + + // Add controller manufacturer if available + if (!string.IsNullOrEmpty(ddcCiIdentity.ControllerManufacturer)) + { + nameParts.Add(ddcCiIdentity.ControllerManufacturer); + } + + // Try to extract model information from capabilities string + var modelFromCaps = ExtractModelFromCapabilities(ddcCiIdentity.CapabilitiesString); + if (!string.IsNullOrEmpty(modelFromCaps)) + { + nameParts.Add(modelFromCaps); + } + + // Add firmware/version info if available and no model found + if (nameParts.Count == 1) // Only manufacturer so far + { + if (!string.IsNullOrEmpty(ddcCiIdentity.FirmwareLevel)) + { + nameParts.Add($"FW{ddcCiIdentity.FirmwareLevel}"); + } + else if (!string.IsNullOrEmpty(ddcCiIdentity.ControllerVersion)) + { + nameParts.Add($"v{ddcCiIdentity.ControllerVersion}"); + } + } + + return nameParts.Count > 0 ? string.Join(" ", nameParts) : string.Empty; + } + + /// + /// Builds a monitor name from EDID information + /// + /// EDID information + /// EDID-derived monitor name + private static string BuildEdidName(ParsedEdidInfo edidInfo) + { + var nameParts = new List(); + + // Add manufacturer name (prefer full name over code) + if (!string.IsNullOrEmpty(edidInfo.ManufacturerName) && + edidInfo.ManufacturerName != edidInfo.ManufacturerCode) + { + nameParts.Add(edidInfo.ManufacturerName); + } + else if (!string.IsNullOrEmpty(edidInfo.ManufacturerCode)) + { + nameParts.Add(edidInfo.ManufacturerCode); + } + + // Try to get model name from raw EDID data + var modelName = EdidParser.ParseModelName(edidInfo.RawData); + if (!string.IsNullOrEmpty(modelName)) + { + nameParts.Add(modelName); + } + else if (edidInfo.ProductCode != 0) + { + // Fallback to product code if no model name + nameParts.Add($"0x{edidInfo.ProductCode:X4}"); + } + + return nameParts.Count > 0 ? string.Join(" ", nameParts) : string.Empty; + } + + /// + /// Attempts to extract model information from DDC/CI capabilities string + /// + /// DDC/CI capabilities string + /// Model name if found, null otherwise + private static string? ExtractModelFromCapabilities(string? capabilitiesString) + { + if (string.IsNullOrEmpty(capabilitiesString)) + return null; + + try + { + // DDC/CI capabilities strings often contain model information + // Format varies but may include model names or codes + // Example: "(prot(monitor)type(LCD)model(VG248QE)cmds(01 02 03 07 0C E3 F3)vcp(10 12 14(05 08 0B) 16 18 1A 52 60(11 12) AC AE B2 B6 C6 C8 C9 D6(01 04) DF)mswhql(1))" + + // Look for model() tag + var modelMatch = System.Text.RegularExpressions.Regex.Match( + capabilitiesString, + @"model\(([^)]+)\)", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (modelMatch.Success) + { + return modelMatch.Groups[1].Value.Trim(); + } + + // Look for type() tag as fallback + var typeMatch = System.Text.RegularExpressions.Regex.Match( + capabilitiesString, + @"type\(([^)]+)\)", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (typeMatch.Success) + { + var typeValue = typeMatch.Groups[1].Value.Trim(); + if (typeValue != "LCD" && typeValue != "CRT") // Skip generic types + { + return typeValue; + } + } + + return null; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/NativeMethods.cs index da45c42..d287ded 100644 --- a/DDCSwitch/NativeMethods.cs +++ b/DDCSwitch/NativeMethods.cs @@ -69,6 +69,38 @@ public static extern bool DestroyPhysicalMonitors( uint dwPhysicalMonitorArraySize, PHYSICAL_MONITOR[] pPhysicalMonitorArray); + // Additional Windows API for getting EDID directly from monitor handle + [DllImport("gdi32.dll", SetLastError = true)] + public static extern IntPtr CreateDC( + string? lpszDriver, + string lpszDevice, + string? lpszOutput, + IntPtr lpInitData); + + [DllImport("gdi32.dll", SetLastError = true)] + public static extern bool DeleteDC(IntPtr hdc); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr MonitorFromPoint( + POINT pt, + uint dwFlags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr MonitorFromWindow( + IntPtr hwnd, + uint dwFlags); + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + } + + public const uint MONITOR_DEFAULTTONULL = 0x00000000; + public const uint MONITOR_DEFAULTTOPRIMARY = 0x00000001; + public const uint MONITOR_DEFAULTTONEAREST = 0x00000002; + // Monitor info structures [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); @@ -88,10 +120,344 @@ public struct MONITORINFOEX public const uint MONITORINFOF_PRIMARY = 0x00000001; /// - /// Attempts to retrieve EDID data for a monitor from Windows Registry. + /// Attempts to retrieve EDID data for a specific physical monitor using enhanced registry mapping. + /// This method properly maps physical monitors to their corresponding registry entries by matching monitor descriptions. /// /// Device name from MONITORINFOEX (e.g., \\.\DISPLAY1) + /// Physical monitor handle for precise mapping + /// Physical monitor description from Windows /// EDID byte array or null if not found + public static byte[]? GetEdidFromRegistryEnhanced(string deviceName, IntPtr physicalMonitorHandle, string? physicalMonitorDescription = null) + { + try + { + // Get all available EDID entries from registry + var allEdidEntries = GetAllRegistryEdidEntries(); + if (allEdidEntries.Count == 0) + return null; + + // Try to match by monitor description if available + if (!string.IsNullOrEmpty(physicalMonitorDescription)) + { + var matchingEntry = FindEdidByDescription(allEdidEntries, physicalMonitorDescription); + if (matchingEntry.HasValue) + { + return matchingEntry.Value.edidData; + } + } + + // For "Generic PnP Monitor", use process of elimination + if (physicalMonitorDescription == "Generic PnP Monitor") + { + var remainingEntry = FindRemainingEdidEntry(allEdidEntries); + if (remainingEntry.HasValue) + { + return remainingEntry.Value.edidData; + } + } + + // Fallback: Use device name based mapping with better heuristics + return GetEdidByDeviceNameHeuristic(deviceName, allEdidEntries); + } + catch + { + // Fallback to original method if enhanced method fails + return GetEdidFromRegistry(deviceName); + } + } + + /// + /// Finds the remaining EDID entry by process of elimination. + /// This is used for "Generic PnP Monitor" which doesn't have a descriptive name. + /// + /// Available EDID entries + /// The remaining EDID entry that hasn't been matched yet + private static (byte[] edidData, DateTime lastWriteTime, string registryPath)? FindRemainingEdidEntry( + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries) + { + // Get all currently active monitors to see which ones have been matched + var activeMonitors = GetCurrentPhysicalMonitors(); + + // Filter out entries that would be matched by other monitors + var unmatchedEntries = new List<(byte[] edidData, DateTime lastWriteTime, string registryPath)>(); + + foreach (var entry in edidEntries) + { + var modelName = EdidParser.ParseModelName(entry.edidData); + var manufacturerName = EdidParser.GetManufacturerName(EdidParser.ParseManufacturerId(entry.edidData)); + + // Skip entries that would be matched by descriptive monitor names + bool wouldBeMatched = false; + + foreach (var (handle, description) in activeMonitors) + { + if (description != "Generic PnP Monitor" && !string.IsNullOrEmpty(modelName)) + { + if (description.Contains(modelName, StringComparison.OrdinalIgnoreCase) || + (!string.IsNullOrEmpty(manufacturerName) && description.Contains(manufacturerName, StringComparison.OrdinalIgnoreCase))) + { + wouldBeMatched = true; + break; + } + } + } + + if (!wouldBeMatched) + { + unmatchedEntries.Add(entry); + } + } + + // Return the most recent unmatched entry + return unmatchedEntries + .OrderByDescending(e => e.lastWriteTime) + .FirstOrDefault(); + } + + /// + /// Finds EDID entry that matches a physical monitor description. + /// + /// Available EDID entries + /// Physical monitor description from Windows + /// Matching EDID entry or null + private static (byte[] edidData, DateTime lastWriteTime, string registryPath)? FindEdidByDescription( + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries, + string description) + { + // Try exact model name matches first + foreach (var entry in edidEntries) + { + var modelName = EdidParser.ParseModelName(entry.edidData); + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + var manufacturerName = EdidParser.GetManufacturerName(manufacturerId); + + if (!string.IsNullOrEmpty(modelName)) + { + // Check if description contains the model name + if (description.Contains(modelName, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + } + + // Try manufacturer name matches + foreach (var entry in edidEntries) + { + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + var manufacturerName = EdidParser.GetManufacturerName(manufacturerId); + + if (!string.IsNullOrEmpty(manufacturerName)) + { + // Check if description contains manufacturer name + if (description.Contains(manufacturerName, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + } + + // Try manufacturer ID matches + foreach (var entry in edidEntries) + { + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + if (!string.IsNullOrEmpty(manufacturerId)) + { + // Check if description contains manufacturer ID + if (description.Contains(manufacturerId, StringComparison.OrdinalIgnoreCase)) + { + return entry; + } + } + } + + return null; + } + + /// + /// Gets EDID data using device name with improved heuristics. + /// + /// Device name + /// Available EDID entries + /// EDID data or null + private static byte[]? GetEdidByDeviceNameHeuristic(string deviceName, + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries) + { + // Extract display index from device name + string displayNum = deviceName.Replace(@"\\.\DISPLAY", ""); + if (!int.TryParse(displayNum, out int displayIndex)) + return null; + + // Get currently active monitors to determine proper mapping + var activeMonitors = GetCurrentPhysicalMonitors(); + int monitorIndex = displayIndex - 1; // Convert to 0-based + + if (monitorIndex < 0 || monitorIndex >= activeMonitors.Count) + return null; + + // Sort EDID entries by recency and uniqueness + var uniqueEdids = FilterUniqueEdidEntries(edidEntries); + var sortedEdids = uniqueEdids + .OrderByDescending(e => e.lastWriteTime) + .Take(activeMonitors.Count) + .ToList(); + + // Map by index among the most recent unique entries + if (monitorIndex < sortedEdids.Count) + { + return sortedEdids[monitorIndex].edidData; + } + + return null; + } + + /// + /// Filters EDID entries to remove duplicates based on manufacturer and product signatures. + /// + /// All EDID entries + /// Filtered unique EDID entries + private static List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> FilterUniqueEdidEntries( + List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> edidEntries) + { + var uniqueEntries = new List<(byte[] edidData, DateTime lastWriteTime, string registryPath)>(); + var seenSignatures = new HashSet(); + + foreach (var entry in edidEntries.OrderByDescending(e => e.lastWriteTime)) + { + var manufacturerId = EdidParser.ParseManufacturerId(entry.edidData); + var productCode = EdidParser.ParseProductCode(entry.edidData); + var serialNumber = EdidParser.ParseNumericSerialNumber(entry.edidData); + + // Create a unique signature including serial number for better uniqueness + var signature = $"{manufacturerId}_{productCode:X4}_{serialNumber}"; + + if (!seenSignatures.Contains(signature)) + { + seenSignatures.Add(signature); + uniqueEntries.Add(entry); + } + } + + return uniqueEntries; + } + + /// + /// Gets all EDID entries from the registry with metadata. + /// + /// List of EDID entries with timestamps + private static List<(byte[] edidData, DateTime lastWriteTime, string registryPath)> GetAllRegistryEdidEntries() + { + var entries = new List<(byte[] edidData, DateTime lastWriteTime, string registryPath)>(); + + try + { + const string displayKey = @"SYSTEM\CurrentControlSet\Enum\DISPLAY"; + using var displayRoot = Registry.LocalMachine.OpenSubKey(displayKey); + if (displayRoot == null) return entries; + + foreach (string mfgKey in displayRoot.GetSubKeyNames()) + { + using var mfgSubKey = displayRoot.OpenSubKey(mfgKey); + if (mfgSubKey == null) continue; + + foreach (string instanceKey in mfgSubKey.GetSubKeyNames()) + { + using var instanceSubKey = mfgSubKey.OpenSubKey(instanceKey); + if (instanceSubKey == null) continue; + + using var deviceParams = instanceSubKey.OpenSubKey("Device Parameters"); + if (deviceParams == null) continue; + + var edidData = deviceParams.GetValue("EDID") as byte[]; + if (edidData == null || edidData.Length < 128) + continue; + + if (!EdidParser.ValidateHeader(edidData)) + continue; + + // Use a heuristic for last write time based on registry structure + var lastWriteTime = EstimateRegistryEntryAge(instanceSubKey); + var registryPath = $@"{displayKey}\{mfgKey}\{instanceKey}"; + + entries.Add((edidData, lastWriteTime, registryPath)); + } + } + } + catch + { + // Return partial results + } + + return entries; + } + + /// + /// Gets information about currently active physical monitors. + /// + /// List of active monitor information + private static List<(IntPtr handle, string description)> GetCurrentPhysicalMonitors() + { + var monitors = new List<(IntPtr handle, string description)>(); + + try + { + NativeMethods.MonitorEnumProc callback = (IntPtr hMonitor, IntPtr hdcMonitor, ref NativeMethods.RECT lprcMonitor, IntPtr dwData) => + { + if (NativeMethods.GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint count) && count > 0) + { + var physicalMonitors = new NativeMethods.PHYSICAL_MONITOR[count]; + if (NativeMethods.GetPhysicalMonitorsFromHMONITOR(hMonitor, count, physicalMonitors)) + { + foreach (var pm in physicalMonitors) + { + monitors.Add((pm.hPhysicalMonitor, pm.szPhysicalMonitorDescription)); + } + } + } + return true; + }; + + NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero); + } + catch + { + // Return partial results + } + + return monitors; + } + + /// + /// Estimates the age of a registry entry using heuristics. + /// + /// Registry key to analyze + /// Estimated last modification time + private static DateTime EstimateRegistryEntryAge(RegistryKey key) + { + try + { + // Heuristic: entries with more subkeys/values are likely more recent + var subKeyCount = key.GetSubKeyNames().Length; + var valueCount = key.GetValueNames().Length; + + if (subKeyCount > 5 || valueCount > 10) + { + return DateTime.Now.AddDays(-1); // Very recent + } + else if (subKeyCount > 0 || valueCount > 5) + { + return DateTime.Now.AddDays(-7); // Recent + } + else + { + return DateTime.Now.AddDays(-30); // Older + } + } + catch + { + return DateTime.Now.AddDays(-30); + } + } public static byte[]? GetEdidFromRegistry(string deviceName) { try diff --git a/DDCSwitch/VcpAnalyzer.cs b/DDCSwitch/VcpAnalyzer.cs new file mode 100644 index 0000000..e58804a --- /dev/null +++ b/DDCSwitch/VcpAnalyzer.cs @@ -0,0 +1,389 @@ +namespace DDCSwitch; + +/// +/// DDC/CI communication status levels +/// +public enum DdcCiStatus +{ + /// + /// Monitor responds to all tested VCP codes + /// + FullyResponsive, + + /// + /// Monitor responds to some but not all VCP codes + /// + PartiallyResponsive, + + /// + /// Monitor does not respond to any VCP codes + /// + NonResponsive, + + /// + /// Communication error occurred during testing + /// + CommunicationError, + + /// + /// Status could not be determined + /// + Unknown +} + +/// +/// VCP version information +/// +public record VcpVersionInfo( + byte MajorVersion, + byte MinorVersion, + bool IsValid +) +{ + public override string ToString() => IsValid ? $"{MajorVersion}.{MinorVersion}" : "Unknown"; +} + +/// +/// Result of testing a specific VCP code +/// +public record VcpTestResult( + byte VcpCode, + string FeatureName, + bool Success, + uint CurrentValue, + uint MaxValue, + string? ErrorMessage +); + +/// +/// Comprehensive VCP capability information +/// +public record VcpCapabilityInfo( + VcpVersionInfo Version, + string? ControllerManufacturer, + string? FirmwareVersion, + Dictionary SupportedFeatures, + bool SupportsNullResponse, + DdcCiStatus DdcCiStatus, + List TestResults +); + +/// +/// Provides comprehensive VCP analysis and DDC/CI capability testing +/// +public static class VcpAnalyzer +{ + // VCP codes for comprehensive testing + private static readonly byte[] TestVcpCodes = new byte[] + { + 0x10, // Brightness + 0x12, // Contrast + 0x60, // Input Source + 0x62, // Audio Volume + 0x8D, // Audio Mute + 0xDF, // VCP Version + 0xC2, // Firmware Level + 0xC4, // Controller Manufacturer + 0xC8 // Controller Version + }; + + // Retry configuration + private const int MaxRetries = 3; + private const int RetryDelayMs = 50; + + /// + /// Performs comprehensive analysis of monitor VCP capabilities + /// + /// Monitor to analyze + /// Comprehensive VCP capability information + public static VcpCapabilityInfo AnalyzeCapabilities(Monitor monitor) + { + if (monitor == null) + { + return new VcpCapabilityInfo( + new VcpVersionInfo(0, 0, false), + null, + null, + new Dictionary(), + false, + DdcCiStatus.Unknown, + new List() + ); + } + + // Get VCP version information + var vcpVersion = GetVcpVersion(monitor); + + // Get controller information via DDC/CI + var ddcCiIdentity = DdcCiMonitorIdentifier.GetIdentityViaDdcCi(monitor); + string? controllerManufacturer = ddcCiIdentity?.ControllerManufacturer; + string? firmwareVersion = ddcCiIdentity?.FirmwareLevel; + + // Perform comprehensive DDC/CI testing + var ddcCiStatus = TestDdcCiComprehensive(monitor); + + // Test multiple VCP codes for responsiveness + var testResults = new List(); + bool multipleVcpSuccess = TestMultipleVcpCodes(monitor, TestVcpCodes, testResults); + + // Test null response support + bool supportsNullResponse = TestNullResponseSupport(monitor); + + // Get supported features using existing scan functionality + var supportedFeatures = monitor.ScanVcpFeatures(); + + return new VcpCapabilityInfo( + vcpVersion, + controllerManufacturer, + firmwareVersion, + supportedFeatures, + supportsNullResponse, + ddcCiStatus, + testResults + ); + } + + /// + /// Determines VCP version supported by the monitor + /// + /// Monitor to query + /// VCP version information + public static VcpVersionInfo GetVcpVersion(Monitor monitor) + { + if (monitor == null) + return new VcpVersionInfo(0, 0, false); + + try + { + // VCP Version is at code 0xDF + if (monitor.TryGetVcpFeature(0xDF, out uint value, out _)) + { + // VCP version is typically encoded as major.minor in BCD or binary + byte major = (byte)((value >> 8) & 0xFF); + byte minor = (byte)(value & 0xFF); + + // Validate reasonable version numbers + if (major >= 1 && major <= 10 && minor <= 99) + { + return new VcpVersionInfo(major, minor, true); + } + + // Try BCD decoding + if ((major & 0xF0) <= 0x90 && (major & 0x0F) <= 0x09 && + (minor & 0xF0) <= 0x90 && (minor & 0x0F) <= 0x09) + { + byte majorBcd = (byte)(((major >> 4) * 10) + (major & 0x0F)); + byte minorBcd = (byte)(((minor >> 4) * 10) + (minor & 0x0F)); + + if (majorBcd >= 1 && majorBcd <= 10 && minorBcd <= 99) + { + return new VcpVersionInfo(majorBcd, minorBcd, true); + } + } + } + + return new VcpVersionInfo(0, 0, false); + } + catch + { + return new VcpVersionInfo(0, 0, false); + } + } + + /// + /// Performs comprehensive DDC/CI testing to determine communication status + /// + /// Monitor to test + /// DDC/CI communication status + public static DdcCiStatus TestDdcCiComprehensive(Monitor monitor) + { + if (monitor == null) + return DdcCiStatus.Unknown; + + try + { + var testResults = new List(); + bool anySuccess = TestMultipleVcpCodes(monitor, TestVcpCodes, testResults); + + if (!anySuccess) + { + return DdcCiStatus.NonResponsive; + } + + // Count successful tests + int successCount = testResults.Count(r => r.Success); + int totalTests = testResults.Count; + + // Determine status based on success ratio + if (successCount == totalTests) + { + return DdcCiStatus.FullyResponsive; + } + else if (successCount > 0) + { + return DdcCiStatus.PartiallyResponsive; + } + else + { + return DdcCiStatus.NonResponsive; + } + } + catch + { + return DdcCiStatus.CommunicationError; + } + } + + /// + /// Tests multiple VCP codes to determine monitor responsiveness + /// + /// Monitor to test + /// Array of VCP codes to test + /// List to store test results (optional) + /// True if any VCP code responded successfully + public static bool TestMultipleVcpCodes(Monitor monitor, byte[] testCodes, List? results = null) + { + if (monitor == null || testCodes == null) + return false; + + bool anySuccess = false; + + foreach (byte vcpCode in testCodes) + { + var testResult = TestSingleVcpCode(monitor, vcpCode); + results?.Add(testResult); + + if (testResult.Success) + { + anySuccess = true; + } + } + + return anySuccess; + } + + /// + /// Tests a single VCP code with retry logic for timing-sensitive monitors + /// + /// Monitor to test + /// VCP code to test + /// Test result + private static VcpTestResult TestSingleVcpCode(Monitor monitor, byte vcpCode) + { + string featureName = FeatureResolver.GetFeatureByCode(vcpCode).Name; + + for (int attempt = 0; attempt < MaxRetries; attempt++) + { + try + { + if (monitor.TryGetVcpFeature(vcpCode, out uint currentValue, out uint maxValue, out int errorCode)) + { + return new VcpTestResult( + vcpCode, + featureName, + true, + currentValue, + maxValue, + null + ); + } + + // If this is not the last attempt, wait before retrying + if (attempt < MaxRetries - 1) + { + Thread.Sleep(RetryDelayMs); + } + } + catch (Exception ex) + { + if (attempt == MaxRetries - 1) + { + return new VcpTestResult( + vcpCode, + featureName, + false, + 0, + 0, + ex.Message + ); + } + + Thread.Sleep(RetryDelayMs); + } + } + + return new VcpTestResult( + vcpCode, + featureName, + false, + 0, + 0, + "Failed after multiple retries" + ); + } + + /// + /// Tests DDC null response behavior for unsupported features + /// + /// Monitor to test + /// True if monitor supports null responses for unsupported features + private static bool TestNullResponseSupport(Monitor monitor) + { + if (monitor == null) + return false; + + try + { + // Test with a VCP code that's unlikely to be supported (0xFF) + // A monitor that supports null responses should return false without error + // A monitor that doesn't support null responses might hang or return an error + + var startTime = DateTime.UtcNow; + bool result = monitor.TryGetVcpFeature(0xFF, out _, out _); + var elapsed = DateTime.UtcNow - startTime; + + // If the call completed quickly (< 100ms), the monitor likely supports null responses + // If it took longer, the monitor might not handle unsupported codes gracefully + return elapsed.TotalMilliseconds < 100; + } + catch + { + // If an exception occurred, the monitor doesn't handle null responses well + return false; + } + } + + /// + /// Gets a human-readable description of the DDC/CI status + /// + /// DDC/CI status + /// Human-readable description + public static string GetStatusDescription(DdcCiStatus status) + { + return status switch + { + DdcCiStatus.FullyResponsive => "Fully responsive to DDC/CI commands", + DdcCiStatus.PartiallyResponsive => "Partially responsive to DDC/CI commands", + DdcCiStatus.NonResponsive => "Does not respond to DDC/CI commands", + DdcCiStatus.CommunicationError => "Communication error during DDC/CI testing", + DdcCiStatus.Unknown => "DDC/CI status unknown", + _ => "Unknown status" + }; + } + + /// + /// Gets a summary of VCP test results + /// + /// List of test results + /// Summary string + public static string GetTestResultsSummary(List testResults) + { + if (testResults == null || testResults.Count == 0) + return "No tests performed"; + + int successCount = testResults.Count(r => r.Success); + int totalCount = testResults.Count; + + return $"{successCount}/{totalCount} VCP codes responded successfully"; + } +} \ No newline at end of file diff --git a/README.md b/README.md index 47a72a3..8f7dbd9 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,16 @@ Contributions welcome! Please open issues for bugs or feature requests. MIT License - see LICENSE file for details +## Disclaimer and Warranty + +**NO WARRANTY**: This software is provided "AS IS" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. The entire risk as to the quality and performance of the software is with you. + +**LIMITATION OF LIABILITY**: In no event shall the authors, copyright holders, or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +**MONITOR DAMAGE**: The authors and contributors of this software are not responsible for any damage to monitors, display devices, or other hardware that may result from the use of this software. DDC/CI commands can potentially affect monitor settings in ways that may cause temporary or permanent changes to display behavior. Users assume all risk when using this software to control monitor settings. + +**USE AT YOUR OWN RISK**: By using this software, you acknowledge that you understand the risks involved in sending DDC/CI commands to your monitors and that you use this software entirely at your own risk. It is recommended to test commands carefully and ensure you can restore your monitor settings manually if needed. + ## Acknowledgments - Inspired by [ddcutil](https://www.ddcutil.com) for Linux From 3a75d1812c6d6be4deb2ecb7b3d5905636c965ff Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:48:19 -0600 Subject: [PATCH 17/17] Restructure project layout --- .gitignore | 4 +- DDCSwitch/{ => Core}/Monitor.cs | 0 DDCSwitch/{ => Core}/MonitorController.cs | 0 DDCSwitch/{ => Core}/NativeMethods.cs | 0 DDCSwitch/DDCSwitch.csproj | 2 +- DDCSwitch/{ => Hardware}/EdidParser.cs | 0 DDCSwitch/{ => Hardware}/HardwareInspector.cs | 0 .../DdcCiMonitorIdentifier.cs | 0 .../MonitorNameResolver.cs | 0 .../{ => VcpFeatures}/FeatureResolver.cs | 0 DDCSwitch/{ => VcpFeatures}/InputSource.cs | 0 DDCSwitch/{ => VcpFeatures}/VcpAnalyzer.cs | 0 .../{ => VcpFeatures}/VcpErrorHandler.cs | 0 .../{ => VcpFeatures}/VcpFeature.Generated.cs | 0 DDCSwitch/{ => VcpFeatures}/VcpFeature.cs | 0 EXAMPLES.md | 1055 ++++++++++++++++- tools/GenerateVcpFeatures.py | 2 +- 17 files changed, 1059 insertions(+), 4 deletions(-) rename DDCSwitch/{ => Core}/Monitor.cs (100%) rename DDCSwitch/{ => Core}/MonitorController.cs (100%) rename DDCSwitch/{ => Core}/NativeMethods.cs (100%) rename DDCSwitch/{ => Hardware}/EdidParser.cs (100%) rename DDCSwitch/{ => Hardware}/HardwareInspector.cs (100%) rename DDCSwitch/{ => Identification}/DdcCiMonitorIdentifier.cs (100%) rename DDCSwitch/{ => Identification}/MonitorNameResolver.cs (100%) rename DDCSwitch/{ => VcpFeatures}/FeatureResolver.cs (100%) rename DDCSwitch/{ => VcpFeatures}/InputSource.cs (100%) rename DDCSwitch/{ => VcpFeatures}/VcpAnalyzer.cs (100%) rename DDCSwitch/{ => VcpFeatures}/VcpErrorHandler.cs (100%) rename DDCSwitch/{ => VcpFeatures}/VcpFeature.Generated.cs (100%) rename DDCSwitch/{ => VcpFeatures}/VcpFeature.cs (100%) diff --git a/.gitignore b/.gitignore index 2005deb..e6995f0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ artifacts/ *.snupkg **/packages/* -dist/ \ No newline at end of file +dist/ + +.vscode/ \ No newline at end of file diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Core/Monitor.cs similarity index 100% rename from DDCSwitch/Monitor.cs rename to DDCSwitch/Core/Monitor.cs diff --git a/DDCSwitch/MonitorController.cs b/DDCSwitch/Core/MonitorController.cs similarity index 100% rename from DDCSwitch/MonitorController.cs rename to DDCSwitch/Core/MonitorController.cs diff --git a/DDCSwitch/NativeMethods.cs b/DDCSwitch/Core/NativeMethods.cs similarity index 100% rename from DDCSwitch/NativeMethods.cs rename to DDCSwitch/Core/NativeMethods.cs diff --git a/DDCSwitch/DDCSwitch.csproj b/DDCSwitch/DDCSwitch.csproj index 835173e..e4607e7 100644 --- a/DDCSwitch/DDCSwitch.csproj +++ b/DDCSwitch/DDCSwitch.csproj @@ -47,7 +47,7 @@ - + True True VcpFeatureData.json diff --git a/DDCSwitch/EdidParser.cs b/DDCSwitch/Hardware/EdidParser.cs similarity index 100% rename from DDCSwitch/EdidParser.cs rename to DDCSwitch/Hardware/EdidParser.cs diff --git a/DDCSwitch/HardwareInspector.cs b/DDCSwitch/Hardware/HardwareInspector.cs similarity index 100% rename from DDCSwitch/HardwareInspector.cs rename to DDCSwitch/Hardware/HardwareInspector.cs diff --git a/DDCSwitch/DdcCiMonitorIdentifier.cs b/DDCSwitch/Identification/DdcCiMonitorIdentifier.cs similarity index 100% rename from DDCSwitch/DdcCiMonitorIdentifier.cs rename to DDCSwitch/Identification/DdcCiMonitorIdentifier.cs diff --git a/DDCSwitch/MonitorNameResolver.cs b/DDCSwitch/Identification/MonitorNameResolver.cs similarity index 100% rename from DDCSwitch/MonitorNameResolver.cs rename to DDCSwitch/Identification/MonitorNameResolver.cs diff --git a/DDCSwitch/FeatureResolver.cs b/DDCSwitch/VcpFeatures/FeatureResolver.cs similarity index 100% rename from DDCSwitch/FeatureResolver.cs rename to DDCSwitch/VcpFeatures/FeatureResolver.cs diff --git a/DDCSwitch/InputSource.cs b/DDCSwitch/VcpFeatures/InputSource.cs similarity index 100% rename from DDCSwitch/InputSource.cs rename to DDCSwitch/VcpFeatures/InputSource.cs diff --git a/DDCSwitch/VcpAnalyzer.cs b/DDCSwitch/VcpFeatures/VcpAnalyzer.cs similarity index 100% rename from DDCSwitch/VcpAnalyzer.cs rename to DDCSwitch/VcpFeatures/VcpAnalyzer.cs diff --git a/DDCSwitch/VcpErrorHandler.cs b/DDCSwitch/VcpFeatures/VcpErrorHandler.cs similarity index 100% rename from DDCSwitch/VcpErrorHandler.cs rename to DDCSwitch/VcpFeatures/VcpErrorHandler.cs diff --git a/DDCSwitch/VcpFeature.Generated.cs b/DDCSwitch/VcpFeatures/VcpFeature.Generated.cs similarity index 100% rename from DDCSwitch/VcpFeature.Generated.cs rename to DDCSwitch/VcpFeatures/VcpFeature.Generated.cs diff --git a/DDCSwitch/VcpFeature.cs b/DDCSwitch/VcpFeatures/VcpFeature.cs similarity index 100% rename from DDCSwitch/VcpFeature.cs rename to DDCSwitch/VcpFeatures/VcpFeature.cs diff --git a/EXAMPLES.md b/EXAMPLES.md index dc77bd8..b7aad2a 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,6 +1,8 @@ # ddcswitch Examples -This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, EDID information retrieval, and automation. +This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, EDID information retrieval, automation, and creative Windows integrations. + +ddcswitch's JSON output support and comprehensive VCP feature access opens up unique automation possibilities. ## Monitor Information (EDID) @@ -1909,3 +1911,1054 @@ Write-Host "Synchronized $($okMonitors.Count) monitors" -ForegroundColor Cyan Happy switching and brightness controlling! +## Creative Windows Integrations + +### Windows Subsystem for Linux (WSL) Integration + +Control Windows monitors from within WSL using ddcswitch: + +```bash +# ~/.bashrc in WSL +alias ddc='/mnt/c/Tools/ddcswitch.exe' +alias ddc-work='ddc set 0 DP1 && ddc set 0 brightness 60%' +alias ddc-game='ddc set 0 HDMI1 && ddc set 0 brightness 90%' + +# Function to get monitor info in WSL +monitor_status() { + /mnt/c/Tools/ddcswitch.exe list --json | jq '.monitors[] | {index, name, currentInput, brightness, contrast}' +} + +# Brightness control from Linux terminal +set_brightness() { + /mnt/c/Tools/ddcswitch.exe set 0 brightness "$1%" + echo "Brightness set to $1%" +} +``` + +### Windows Terminal Custom Actions + +Add ddcswitch commands to Windows Terminal settings.json: + +```json +{ + "actions": [ + { + "command": { + "action": "wt", + "commandline": "ddcswitch set 0 HDMI1" + }, + "keys": "ctrl+alt+h", + "name": "Switch to HDMI" + }, + { + "command": { + "action": "wt", + "commandline": "ddcswitch set 0 DP1" + }, + "keys": "ctrl+alt+d", + "name": "Switch to DisplayPort" + }, + { + "command": { + "action": "wt", + "commandline": "ddcswitch list --verbose" + }, + "keys": "ctrl+alt+m", + "name": "Monitor Status" + } + ] +} +``` + +### Microsoft Power Automate Desktop Integration + +Create automated workflows that respond to system events: + +``` +# Power Automate Desktop Flow: "Smart Monitor Control" + +# Trigger: When specific application launches +IF Application.IsProcessRunning ProcessName: 'GameLauncher' THEN + # Switch to gaming setup + System.RunDOSCommand Command: 'ddcswitch set 0 HDMI1' + System.RunDOSCommand Command: 'ddcswitch set 0 brightness 90%' + System.RunDOSCommand Command: 'ddcswitch set 0 contrast 85%' + + # Show notification + Display.ShowNotification Title: 'Gaming Mode' Message: 'Monitors configured for gaming' +END + +# Trigger: When work hours begin (9 AM) +IF DateTime.Now.Hour = 9 THEN + System.RunDOSCommand Command: 'ddcswitch set 0 DP1' + System.RunDOSCommand Command: 'ddcswitch set 0 brightness 60%' + Display.ShowNotification Title: 'Work Mode' Message: 'Monitors ready for productivity' +END +``` + +### Windows Event Log Integration + +Monitor system events and respond with display changes: + +```powershell +# monitor-event-handler.ps1 +# Register for system events and adjust monitors accordingly + +Register-WmiEvent -Query "SELECT * FROM Win32_VolumeChangeEvent WHERE EventType = 2" -Action { + # USB device connected - might be a console + Start-Sleep -Seconds 2 + $result = ddcswitch list --json | ConvertFrom-Json + + # Check if we should auto-switch to console input + $gameConsoles = @("Xbox", "PlayStation", "Nintendo") + # Logic to detect console and switch input + ddcswitch set 0 HDMI1 + Write-EventLog -LogName Application -Source "ddcswitch" -EventId 1001 -Message "Auto-switched to console input" +} + +# Register for user session changes +Register-WmiEvent -Query "SELECT * FROM Win32_SessionChangeEvent" -Action { + $event = $Event.SourceEventArgs.NewEvent + + switch ($event.Type) { + 7 { # Session locked + ddcswitch set 0 brightness 10% # Dim when locked + } + 8 { # Session unlocked + ddcswitch set 0 brightness 75% # Restore brightness + } + } +} +``` + +### Razer Synapse / Logitech G HUB Integration + +Use macro keys to control monitors: + +```csharp +// Razer Synapse C# Script for macro key +using System.Diagnostics; + +public void ExecuteMacro() +{ + // Gaming profile macro + Process.Start("ddcswitch.exe", "set 0 HDMI1"); + System.Threading.Thread.Sleep(500); + Process.Start("ddcswitch.exe", "set 0 brightness 90%"); + Process.Start("ddcswitch.exe", "set 0 contrast 85%"); +} +``` + +### OBS Studio Integration + +Control monitor settings during streaming: + +```lua +-- OBS Studio Lua Script: monitor-control.lua +obs = obslua + +function script_description() + return "Control monitor settings during streaming" +end + +function on_scene_switch(event) + local scene_name = obs.obs_frontend_get_current_scene_name() + + if scene_name == "Gaming Scene" then + os.execute("ddcswitch set 0 HDMI1") + os.execute("ddcswitch set 0 brightness 95%") + elseif scene_name == "Just Chatting" then + os.execute("ddcswitch set 0 DP1") + os.execute("ddcswitch set 0 brightness 70%") + end +end + +function script_load(settings) + obs.obs_frontend_add_event_callback(on_scene_switch) +end +``` + +### Discord Bot Integration + +Control monitors via Discord commands: + +```javascript +// discord-monitor-bot.js +const { Client, GatewayIntentBits } = require('discord.js'); +const { execSync } = require('child_process'); + +const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] }); + +client.on('messageCreate', async message => { + if (!message.content.startsWith('!monitor')) return; + + const args = message.content.split(' '); + const command = args[1]; + + try { + switch(command) { + case 'status': + const status = execSync('ddcswitch list --json').toString(); + const data = JSON.parse(status); + const embed = { + title: 'Monitor Status', + fields: data.monitors.map(m => ({ + name: m.name, + value: `Input: ${m.currentInput}\nBrightness: ${m.brightness || 'N/A'}`, + inline: true + })) + }; + message.reply({ embeds: [embed] }); + break; + + case 'gaming': + execSync('ddcswitch set 0 HDMI1'); + execSync('ddcswitch set 0 brightness 90%'); + message.reply('🎮 Gaming mode activated!'); + break; + + case 'work': + execSync('ddcswitch set 0 DP1'); + execSync('ddcswitch set 0 brightness 60%'); + message.reply('💼 Work mode activated!'); + break; + } + } catch (error) { + message.reply('❌ Monitor control failed'); + } +}); + +client.login('YOUR_BOT_TOKEN'); +``` + +### Home Assistant Integration + +Control monitors from your smart home system: + +```yaml +# configuration.yaml +shell_command: + monitor_gaming: "ddcswitch set 0 HDMI1 && ddcswitch set 0 brightness 90%" + monitor_work: "ddcswitch set 0 DP1 && ddcswitch set 0 brightness 60%" + monitor_movie: "ddcswitch set 0 HDMI1 && ddcswitch set 0 brightness 30%" + +sensor: + - platform: command_line + name: monitor_status + command: 'ddcswitch list --json' + value_template: '{{ value_json.monitors[0].currentInput }}' + json_attributes: + - monitors + +automation: + - alias: "Gaming Time" + trigger: + platform: time + at: "19:00:00" + action: + service: shell_command.monitor_gaming + + - alias: "Work Hours" + trigger: + platform: time + at: "09:00:00" + action: + service: shell_command.monitor_work +``` + +### Twitch Chat Bot Integration + +Let viewers control your monitor settings: + +```python +# twitch-monitor-bot.py +import socket +import subprocess +import json +import time + +class TwitchBot: + def __init__(self, token, channel): + self.token = token + self.channel = channel + self.sock = socket.socket() + + def connect(self): + self.sock.connect(('irc.chat.twitch.tv', 6667)) + self.sock.send(f"PASS {self.token}\n".encode('utf-8')) + self.sock.send(f"NICK streambot\n".encode('utf-8')) + self.sock.send(f"JOIN {self.channel}\n".encode('utf-8')) + + def send_message(self, message): + self.sock.send(f"PRIVMSG {self.channel} :{message}\n".encode('utf-8')) + + def run_ddc(self, args): + try: + result = subprocess.run(['ddcswitch'] + args + ['--json'], + capture_output=True, text=True) + return json.loads(result.stdout) + except: + return {'success': False} + + def handle_command(self, user, command): + # Only allow certain users or subscribers + if user not in ['streamer', 'moderator']: + return + + if command == '!brightness_up': + # Get current brightness and increase by 10% + current = self.run_ddc(['get', '0', 'brightness']) + if current['success']: + new_brightness = min(100, current['percentageValue'] + 10) + result = self.run_ddc(['set', '0', 'brightness', f'{new_brightness}%']) + if result['success']: + self.send_message(f"Brightness increased to {new_brightness}%") + + elif command == '!brightness_down': + current = self.run_ddc(['get', '0', 'brightness']) + if current['success']: + new_brightness = max(10, current['percentageValue'] - 10) + result = self.run_ddc(['set', '0', 'brightness', f'{new_brightness}%']) + if result['success']: + self.send_message(f"Brightness decreased to {new_brightness}%") + + elif command == '!monitor_status': + status = self.run_ddc(['list', '--verbose']) + if status['success']: + monitor = status['monitors'][0] + self.send_message(f"Monitor: {monitor['currentInput']}, " + f"Brightness: {monitor.get('brightness', 'N/A')}") + +# Usage +bot = TwitchBot('oauth:your_token', '#your_channel') +bot.connect() +# Add message parsing loop... +``` + +### Windows Sandbox Testing Environment + +Test monitor configurations safely: + +```xml + + + + + C:\Tools + C:\Tools + true + + + + C:\Tools\ddcswitch.exe list --verbose + + +``` + +### Microsoft Graph API Integration + +Control monitors based on calendar events: + +```csharp +// CalendarMonitorController.cs +using Microsoft.Graph; +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +public class CalendarMonitorController +{ + private GraphServiceClient _graphClient; + + public async Task CheckUpcomingMeetings() + { + var events = await _graphClient.Me.Events + .Request() + .Filter($"start/dateTime ge '{DateTime.Now:yyyy-MM-ddTHH:mm:ss.fffK}'") + .Top(1) + .GetAsync(); + + if (events.Any()) + { + var nextMeeting = events.First(); + var timeUntilMeeting = nextMeeting.Start.DateTime - DateTime.Now; + + if (timeUntilMeeting.TotalMinutes <= 5) + { + // Meeting starting soon - optimize for video calls + Process.Start("ddcswitch.exe", "set 0 DP1"); // Switch to PC + Process.Start("ddcswitch.exe", "set 0 brightness 80%"); // Good lighting + Process.Start("ddcswitch.exe", "set 0 contrast 75%"); // Clear video + + // Show notification + var notification = new ToastNotification("Meeting Mode", + "Monitor optimized for video call"); + notification.Show(); + } + } + } +} +``` + +### Windows Performance Toolkit Integration + +Monitor performance impact of display changes: + +```powershell +# performance-monitor.ps1 +# Track system performance during monitor operations + +function Measure-MonitorOperation { + param([string]$Operation) + + # Start performance counter + $cpu = Get-Counter "\Processor(_Total)\% Processor Time" + $startTime = Get-Date + + # Execute monitor command + $result = Invoke-Expression "ddcswitch $Operation --json" | ConvertFrom-Json + + $endTime = Get-Date + $duration = ($endTime - $startTime).TotalMilliseconds + + # Log performance data + $perfData = @{ + Operation = $Operation + Success = $result.success + Duration = $duration + CPUBefore = $cpu.CounterSamples[0].CookedValue + Timestamp = $startTime + } + + $perfData | ConvertTo-Json | Out-File -Append "monitor-performance.log" + + Write-Host "Operation: $Operation completed in ${duration}ms" -ForegroundColor Green +} + +# Test various operations +Measure-MonitorOperation "list" +Measure-MonitorOperation "set 0 HDMI1" +Measure-MonitorOperation "set 0 brightness 75%" +Measure-MonitorOperation "get 0" +``` + +### Chocolatey Package Hooks + +Automatically configure monitors when installing/updating software: + +```powershell +# chocolateyinstall.ps1 for a gaming package +$packageName = 'steam' + +# Install Steam normally... +Install-ChocolateyPackage @packageArgs + +# Configure monitors for gaming after Steam install +if (Get-Command ddcswitch -ErrorAction SilentlyContinue) { + Write-Host "Configuring monitors for gaming..." -ForegroundColor Green + + # Create gaming profile + $gamingScript = @" +ddcswitch set 0 HDMI1 +ddcswitch set 0 brightness 90% +ddcswitch set 0 contrast 85% +Write-Host "Gaming profile activated!" -ForegroundColor Green +"@ + + $gamingScript | Out-File "$env:USERPROFILE\Desktop\Gaming-Mode.ps1" + Write-Host "Created Gaming-Mode.ps1 on desktop" -ForegroundColor Cyan +} +``` + +### Windows Subsystem for Android (WSA) Integration + +Control monitors when Android apps launch: + +```bash +#!/system/bin/sh +# monitor-control.sh (in WSA) + +# Detect when gaming apps launch and signal Windows +am monitor --gdb | while read line; do + if echo "$line" | grep -q "com.epicgames.fortnite"; then + # Signal Windows to switch to gaming mode + /mnt/c/Tools/ddcswitch.exe set 0 HDMI1 + /mnt/c/Tools/ddcswitch.exe set 0 brightness 95% + fi +done +``` + +### Microsoft Intune/MDM Integration + +Deploy monitor configurations across enterprise: + +```xml + + + + + 1 + + + ./Device/Vendor/MSFT/Policy/Config/DeliveryOptimization/DODownloadMode + + + int + + 1 + + + + 2 + + + ./Device/Vendor/MSFT/Policy/Config/Update/ScheduledInstallTime + + + chr + + ddcswitch set 0 brightness 60% + + + + +``` + +### Windows Presentation Foundation (WPF) GUI + +Create a visual interface for ddcswitch: + +```csharp +// MonitorControlGUI.xaml.cs +using System; +using System.Diagnostics; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; + +public partial class MonitorControlWindow : Window +{ + public MonitorControlWindow() + { + InitializeComponent(); + LoadMonitors(); + } + + private void LoadMonitors() + { + try + { + var result = RunDDCCommand("list --json"); + var data = JsonSerializer.Deserialize(result); + + MonitorComboBox.ItemsSource = data.Monitors; + MonitorComboBox.DisplayMemberPath = "Name"; + MonitorComboBox.SelectedValuePath = "Index"; + } + catch (Exception ex) + { + MessageBox.Show($"Failed to load monitors: {ex.Message}"); + } + } + + private void BrightnessSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) + { + if (MonitorComboBox.SelectedValue != null) + { + var monitorIndex = MonitorComboBox.SelectedValue.ToString(); + var brightness = (int)e.NewValue; + + RunDDCCommand($"set {monitorIndex} brightness {brightness}%"); + BrightnessLabel.Content = $"Brightness: {brightness}%"; + } + } + + private void InputButton_Click(object sender, RoutedEventArgs e) + { + var button = sender as Button; + var input = button.Tag.ToString(); + var monitorIndex = MonitorComboBox.SelectedValue.ToString(); + + RunDDCCommand($"set {monitorIndex} {input}"); + StatusLabel.Content = $"Switched to {input}"; + } + + private string RunDDCCommand(string args) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "ddcswitch.exe", + Arguments = args, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + return output; + } +} +``` + +### Windows Registry Integration + +Store and restore monitor preferences: + +```powershell +# registry-monitor-profiles.ps1 +$registryPath = "HKCU:\Software\ddcswitch\Profiles" + +function Save-MonitorProfile { + param([string]$ProfileName) + + # Get current monitor state + $monitors = ddcswitch list --verbose --json | ConvertFrom-Json + + if (-not (Test-Path $registryPath)) { + New-Item -Path $registryPath -Force + } + + foreach ($monitor in $monitors.monitors) { + $profileKey = "$registryPath\$ProfileName\Monitor$($monitor.index)" + New-Item -Path $profileKey -Force + + Set-ItemProperty -Path $profileKey -Name "Input" -Value $monitor.currentInput + if ($monitor.brightness) { + Set-ItemProperty -Path $profileKey -Name "Brightness" -Value $monitor.brightness + } + if ($monitor.contrast) { + Set-ItemProperty -Path $profileKey -Name "Contrast" -Value $monitor.contrast + } + } + + Write-Host "Profile '$ProfileName' saved to registry" -ForegroundColor Green +} + +function Restore-MonitorProfile { + param([string]$ProfileName) + + $profilePath = "$registryPath\$ProfileName" + if (-not (Test-Path $profilePath)) { + Write-Error "Profile '$ProfileName' not found" + return + } + + $monitorKeys = Get-ChildItem -Path $profilePath + foreach ($key in $monitorKeys) { + $monitorIndex = $key.Name -replace '.*Monitor(\d+)', '$1' + + $input = Get-ItemProperty -Path $key.PSPath -Name "Input" -ErrorAction SilentlyContinue + $brightness = Get-ItemProperty -Path $key.PSPath -Name "Brightness" -ErrorAction SilentlyContinue + $contrast = Get-ItemProperty -Path $key.PSPath -Name "Contrast" -ErrorAction SilentlyContinue + + if ($input) { ddcswitch set $monitorIndex $input.Input } + if ($brightness) { ddcswitch set $monitorIndex brightness $brightness.Brightness } + if ($contrast) { ddcswitch set $monitorIndex contrast $contrast.Contrast } + } + + Write-Host "Profile '$ProfileName' restored" -ForegroundColor Green +} + +# Usage: +# Save-MonitorProfile "Gaming" +# Restore-MonitorProfile "Gaming" +``` + +## Windows Productivity Workflows + +### Focus Mode with Windows Focus Assist + +Combine Windows Focus Assist with monitor dimming for deep work: + +```powershell +# focus-mode.ps1 +param([switch]$Enable, [switch]$Disable) + +if ($Enable) { + # Enable Focus Assist (Priority only) + $registryPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\Cache\DefaultAccount" + # Set Focus Assist to Priority only mode + + # Dim all monitors for focus + $monitors = ddcswitch list --json | ConvertFrom-Json + foreach ($monitor in $monitors.monitors) { + if ($monitor.status -eq "ok") { + ddcswitch set $monitor.index brightness 40% + } + } + + Write-Host "🎯 Focus mode enabled - monitors dimmed, notifications limited" -ForegroundColor Blue + + # Set timer for break reminder + Start-Job -ScriptBlock { + Start-Sleep -Seconds 1800 # 30 minutes + [System.Windows.Forms.MessageBox]::Show("Time for a break!", "Focus Mode") + } +} + +if ($Disable) { + # Restore normal brightness + $monitors = ddcswitch list --json | ConvertFrom-Json + foreach ($monitor in $monitors.monitors) { + if ($monitor.status -eq "ok") { + ddcswitch set $monitor.index brightness 75% + } + } + + Write-Host "✨ Focus mode disabled - normal brightness restored" -ForegroundColor Green +} +``` + +### Pomodoro Timer Integration + +Automatically adjust monitor settings during work/break cycles: + +```csharp +// PomodoroMonitorController.cs +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +public class PomodoroMonitorController +{ + private Timer _workTimer; + private Timer _breakTimer; + private bool _isWorkSession = true; + + public void StartPomodoro() + { + StartWorkSession(); + } + + private void StartWorkSession() + { + _isWorkSession = true; + + // Work mode: Higher brightness, focus input + RunDDCCommand("set 0 DP1"); // PC input for work + RunDDCCommand("set 0 brightness 80%"); // Good brightness for productivity + RunDDCCommand("set 0 contrast 75%"); // Comfortable contrast + + ShowNotification("🍅 Work Session Started", "25 minutes of focused work"); + + // Set 25-minute timer + _workTimer = new Timer(OnWorkSessionEnd, null, TimeSpan.FromMinutes(25), Timeout.InfiniteTimeSpan); + } + + private void OnWorkSessionEnd(object state) + { + _workTimer?.Dispose(); + StartBreakSession(); + } + + private void StartBreakSession() + { + _isWorkSession = false; + + // Break mode: Lower brightness, entertainment input + RunDDCCommand("set 0 brightness 50%"); // Dimmer for rest + RunDDCCommand("set 0 contrast 85%"); // Higher contrast for media + + ShowNotification("☕ Break Time!", "5 minutes to rest your eyes"); + + // Set 5-minute timer + _breakTimer = new Timer(OnBreakSessionEnd, null, TimeSpan.FromMinutes(5), Timeout.InfiniteTimeSpan); + } + + private void OnBreakSessionEnd(object state) + { + _breakTimer?.Dispose(); + StartWorkSession(); // Loop back to work + } + + private void RunDDCCommand(string args) + { + Process.Start(new ProcessStartInfo + { + FileName = "ddcswitch.exe", + Arguments = args, + CreateNoWindow = true, + UseShellExecute = false + }); + } + + private void ShowNotification(string title, string message) + { + var notification = new NotifyIcon + { + Icon = SystemIcons.Information, + BalloonTipTitle = title, + BalloonTipText = message, + Visible = true + }; + notification.ShowBalloonTip(3000); + } +} +``` + +### Eye Strain Reduction with Blue Light Filtering + +Combine with Windows Night Light for comprehensive eye care: + +```powershell +# eye-care-scheduler.ps1 +# Automatically adjust monitors throughout the day for eye health + +function Set-EyeCareMode { + param([string]$Mode) + + switch ($Mode) { + "Morning" { + # Bright, energizing settings + ddcswitch set 0 brightness 85% + ddcswitch set 0 contrast 80% + # Enable Windows Night Light (warm colors) + reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount" /v "NightLightEnabled" /t REG_DWORD /d 0 /f + Write-Host "🌅 Morning mode: Bright and energizing" -ForegroundColor Yellow + } + + "Midday" { + # Maximum brightness for productivity + ddcswitch set 0 brightness 90% + ddcswitch set 0 contrast 75% + Write-Host "☀️ Midday mode: Maximum productivity" -ForegroundColor Green + } + + "Evening" { + # Reduced brightness, warmer colors + ddcswitch set 0 brightness 60% + ddcswitch set 0 contrast 70% + # Enable Night Light + reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\DefaultAccount" /v "NightLightEnabled" /t REG_DWORD /d 1 /f + Write-Host "🌆 Evening mode: Reduced strain" -ForegroundColor Orange + } + + "Night" { + # Very dim for late work + ddcswitch set 0 brightness 25% + ddcswitch set 0 contrast 65% + Write-Host "🌙 Night mode: Minimal eye strain" -ForegroundColor Blue + } + } +} + +# Schedule throughout the day +$hour = (Get-Date).Hour + +if ($hour -ge 6 -and $hour -lt 10) { + Set-EyeCareMode "Morning" +} elseif ($hour -ge 10 -and $hour -lt 16) { + Set-EyeCareMode "Midday" +} elseif ($hour -ge 16 -and $hour -lt 20) { + Set-EyeCareMode "Evening" +} else { + Set-EyeCareMode "Night" +} +``` + +### Multi-Monitor Workspace Management + +Intelligent workspace switching based on active applications: + +```powershell +# workspace-manager.ps1 +# Automatically configure monitors based on active applications + +function Get-ActiveApplication { + Add-Type @" + using System; + using System.Runtime.InteropServices; + using System.Text; + public class Win32 { + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll")] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); + } +"@ + + $hwnd = [Win32]::GetForegroundWindow() + $processId = 0 + [Win32]::GetWindowThreadProcessId($hwnd, [ref]$processId) + + return Get-Process -Id $processId -ErrorAction SilentlyContinue +} + +function Set-WorkspaceForApplication { + param([string]$ProcessName) + + $workspaceConfigs = @{ + "Code" = @{ + Description = "Visual Studio Code - Development" + Monitor0 = @{ Input = "DP1"; Brightness = 70; Contrast = 75 } + Monitor1 = @{ Input = "DP2"; Brightness = 65; Contrast = 75 } + } + "chrome" = @{ + Description = "Web Browsing" + Monitor0 = @{ Input = "DP1"; Brightness = 75; Contrast = 80 } + Monitor1 = @{ Input = "DP2"; Brightness = 70; Contrast = 80 } + } + "Photoshop" = @{ + Description = "Photo Editing - Color Accurate" + Monitor0 = @{ Input = "DP1"; Brightness = 80; Contrast = 85 } + Monitor1 = @{ Input = "DP2"; Brightness = 75; Contrast = 85 } + } + "Steam" = @{ + Description = "Gaming Mode" + Monitor0 = @{ Input = "HDMI1"; Brightness = 95; Contrast = 90 } + Monitor1 = @{ Input = "HDMI2"; Brightness = 90; Contrast = 85 } + } + "obs64" = @{ + Description = "Streaming Setup" + Monitor0 = @{ Input = "DP1"; Brightness = 85; Contrast = 80 } + Monitor1 = @{ Input = "HDMI1"; Brightness = 80; Contrast = 85 } + } + } + + if ($workspaceConfigs.ContainsKey($ProcessName)) { + $config = $workspaceConfigs[$ProcessName] + Write-Host "🖥️ Configuring workspace: $($config.Description)" -ForegroundColor Cyan + + # Apply to Monitor 0 + if ($config.Monitor0) { + ddcswitch set 0 $config.Monitor0.Input + ddcswitch set 0 brightness "$($config.Monitor0.Brightness)%" + ddcswitch set 0 contrast "$($config.Monitor0.Contrast)%" + } + + # Apply to Monitor 1 if exists + if ($config.Monitor1) { + $monitors = ddcswitch list --json | ConvertFrom-Json + if ($monitors.monitors.Count -gt 1) { + ddcswitch set 1 $config.Monitor1.Input + ddcswitch set 1 brightness "$($config.Monitor1.Brightness)%" + ddcswitch set 1 contrast "$($config.Monitor1.Contrast)%" + } + } + + return $true + } + + return $false +} + +# Monitor active application and adjust workspace +$lastProcess = "" +while ($true) { + $currentProcess = Get-ActiveApplication + + if ($currentProcess -and $currentProcess.ProcessName -ne $lastProcess) { + $configured = Set-WorkspaceForApplication $currentProcess.ProcessName + if ($configured) { + $lastProcess = $currentProcess.ProcessName + } + } + + Start-Sleep -Seconds 2 +} +``` + +### Meeting Room Display Management + +Automatically configure displays for presentations and video calls: + +```powershell +# meeting-room-controller.ps1 +# Integrate with Outlook calendar for automatic display management + +Add-Type -AssemblyName Microsoft.Office.Interop.Outlook + +function Get-UpcomingMeeting { + $outlook = New-Object -ComObject Outlook.Application + $namespace = $outlook.GetNamespace("MAPI") + $calendar = $namespace.GetDefaultFolder(9) # Calendar folder + + $now = Get-Date + $endTime = $now.AddHours(1) + + $filter = "[Start] >= '$($now.ToString("MM/dd/yyyy HH:mm"))' AND [Start] <= '$($endTime.ToString("MM/dd/yyyy HH:mm"))'" + $appointments = $calendar.Items.Restrict($filter) + + return $appointments | Select-Object -First 1 +} + +function Set-PresentationMode { + Write-Host "📊 Activating Presentation Mode" -ForegroundColor Green + + # High brightness for projector visibility + ddcswitch set 0 brightness 100% + ddcswitch set 0 contrast 90% + + # Switch to presentation input (HDMI for projector) + ddcswitch set 0 HDMI1 + + # Duplicate display for presenter view + DisplaySwitch.exe /duplicate + + # Disable screen saver + powercfg /change standby-timeout-ac 0 + powercfg /change monitor-timeout-ac 0 +} + +function Set-VideoCallMode { + Write-Host "📹 Activating Video Call Mode" -ForegroundColor Blue + + # Optimal brightness for webcam lighting + ddcswitch set 0 brightness 85% + ddcswitch set 0 contrast 80% + + # PC input for video call software + ddcswitch set 0 DP1 + + # Extend displays for notes/chat + DisplaySwitch.exe /extend +} + +function Set-NormalMode { + Write-Host "💼 Returning to Normal Mode" -ForegroundColor Gray + + # Standard work brightness + ddcswitch set 0 brightness 70% + ddcswitch set 0 contrast 75% + + # Restore power settings + powercfg /change standby-timeout-ac 15 + powercfg /change monitor-timeout-ac 10 +} + +# Check for upcoming meetings +$meeting = Get-UpcomingMeeting + +if ($meeting) { + $timeUntilMeeting = ($meeting.Start - (Get-Date)).TotalMinutes + + if ($timeUntilMeeting -le 5 -and $timeUntilMeeting -gt 0) { + if ($meeting.Subject -match "presentation|demo|training") { + Set-PresentationMode + } elseif ($meeting.Subject -match "call|meeting|standup|sync") { + Set-VideoCallMode + } + + # Schedule return to normal mode after meeting + $meetingDuration = ($meeting.End - $meeting.Start).TotalMinutes + Start-Job -ScriptBlock { + param($Duration) + Start-Sleep -Seconds ($Duration * 60) + & "meeting-room-controller.ps1" -RestoreNormal + } -ArgumentList $meetingDuration + } +} +``` + +These creative integrations showcase ddcswitch's unique position in the Windows ecosystem, leveraging its JSON output and comprehensive VCP support for automation scenarios that weren't possible with traditional DDC/CI tools. + diff --git a/tools/GenerateVcpFeatures.py b/tools/GenerateVcpFeatures.py index f2c5355..bce808d 100644 --- a/tools/GenerateVcpFeatures.py +++ b/tools/GenerateVcpFeatures.py @@ -54,7 +54,7 @@ def generate_all_features_list(features): def main(): script_dir = Path(__file__).parent json_file = script_dir.parent / 'DDCSwitch' / 'VcpFeatureData.json' - output_file = script_dir.parent / 'DDCSwitch' / 'VcpFeature.Generated.cs' + output_file = script_dir.parent / 'DDCSwitch' / 'VcpFeatures' / 'VcpFeature.Generated.cs' print(f"Loading {json_file}...") with open(json_file, 'r', encoding='utf-8') as f: