diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 289b72d..dcce49a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -38,7 +38,7 @@ jobs: - name: Verify executable exists shell: pwsh run: | - $exePath = "DDCSwitch/bin/Release/net10.0/win-x64/publish/DDCSwitch.exe" + $exePath = "DDCSwitch/bin/Release/net10.0/win-x64/publish/ddcswitch.exe" if (Test-Path $exePath) { $size = (Get-Item $exePath).Length / 1MB Write-Host "✓ Build successful! Executable size: $([math]::Round($size, 2)) MB" @@ -52,7 +52,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: DDCSwitch-build-${{ github.sha }} - path: DDCSwitch/bin/Release/net10.0/win-x64/publish/DDCSwitch.exe + path: DDCSwitch/bin/Release/net10.0/win-x64/publish/ddcswitch.exe retention-days: 7 release: @@ -92,7 +92,7 @@ jobs: shell: pwsh run: | New-Item -ItemType Directory -Force -Path release - Copy-Item artifact/DDCSwitch.exe release/ + Copy-Item artifact/ddcswitch.exe release/ Copy-Item README.md release/ Copy-Item LICENSE release/ Copy-Item EXAMPLES.md release/ @@ -102,7 +102,7 @@ jobs: shell: pwsh run: | $version = "${{ steps.get_version.outputs.version }}" - Compress-Archive -Path release/* -DestinationPath "DDCSwitch-$version-win-x64.zip" + Compress-Archive -Path release/* -DestinationPath "ddcswitch-$version-win-x64.zip" - name: Generate release notes id: release_notes @@ -150,20 +150,140 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.get_version.outputs.tag }} - name: DDCSwitch ${{ steps.get_version.outputs.version }} + name: ddcswitch ${{ steps.get_version.outputs.version }} body: ${{ steps.release_notes.outputs.notes }} draft: false prerelease: false files: | - DDCSwitch-${{ steps.get_version.outputs.version }}-win-x64.zip - release/DDCSwitch.exe + ddcswitch-${{ steps.get_version.outputs.version }}-win-x64.zip + release/ddcswitch.exe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: DDCSwitch-${{ steps.get_version.outputs.version }}-win-x64 + name: ddcswitch-${{ steps.get_version.outputs.version }}-win-x64 path: release/ retention-days: 30 + chocolatey-package: + needs: release + runs-on: windows-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get version from changelog + id: get_version + shell: pwsh + run: | + $changelog = Get-Content CHANGELOG.md -Raw + if ($changelog -match '\[(\d+\.\d+\.\d+)\]') { + $version = $matches[1] + echo "version=$version" >> $env:GITHUB_OUTPUT + echo "Found version: $version" + } else { + echo "Error: Could not find version in CHANGELOG.md" + exit 1 + } + + - name: Calculate checksum + id: checksum + shell: pwsh + run: | + $version = "${{ steps.get_version.outputs.version }}" + $url = "https://github.com/markdwags/ddcswitch/releases/download/v$version/ddcswitch-$version-win-x64.zip" + $tempFile = "$env:TEMP\ddcswitch-$version.zip" + + Write-Host "Downloading ZIP from: $url" + Start-Sleep -Seconds 2 # Give GitHub a moment to make the release available + + # Retry logic for download + $maxRetries = 5 + $retryCount = 0 + $downloaded = $false + + while (-not $downloaded -and $retryCount -lt $maxRetries) { + try { + Invoke-WebRequest -Uri $url -OutFile $tempFile -ErrorAction Stop + $downloaded = $true + } catch { + $retryCount++ + Write-Host "Download attempt $retryCount failed. Retrying in 10 seconds..." + Start-Sleep -Seconds 10 + } + } + + if (-not $downloaded) { + Write-Host "Failed to download after $maxRetries attempts" + exit 1 + } + + $hash = Get-FileHash $tempFile -Algorithm SHA256 + $checksum = $hash.Hash + echo "checksum=$checksum" >> $env:GITHUB_OUTPUT + Write-Host "SHA256 Checksum: $checksum" + Remove-Item $tempFile + + - name: Update Chocolatey files with version and checksum + shell: pwsh + run: | + $version = "${{ steps.get_version.outputs.version }}" + $checksum = "${{ steps.checksum.outputs.checksum }}" + + # Update version in nuspec (Chocolatey will pass to scripts via $env:chocolateyPackageVersion) + (Get-Content chocolatey\ddcswitch.nuspec -Raw) -replace '__VERSION__', $version | Set-Content chocolatey\ddcswitch.nuspec -NoNewline + + # Create CHECKSUM file for install script to read + Set-Content chocolatey\tools\CHECKSUM $checksum -NoNewline + + # Update VERIFICATION.txt for moderators + (Get-Content chocolatey\tools\VERIFICATION.txt -Raw) -replace '__VERSION__', $version -replace '__CHECKSUM__', $checksum | Set-Content chocolatey\tools\VERIFICATION.txt -NoNewline + + Write-Host "✓ Updated Chocolatey files" + Write-Host " Version: $version (in nuspec, passed via env to scripts)" + Write-Host " Checksum: $checksum (in CHECKSUM file)" + + - name: Install Chocolatey + shell: pwsh + run: | + if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Host "Installing Chocolatey..." + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + } else { + Write-Host "Chocolatey is already installed" + } + + - name: Create Chocolatey package + shell: pwsh + run: | + cd chocolatey + choco pack + $version = "${{ steps.get_version.outputs.version }}" + if (Test-Path "ddcswitch.$version.nupkg") { + Write-Host "✓ Successfully created ddcswitch.$version.nupkg" + } else { + Write-Host "✗ Failed to create package" + exit 1 + } + + - name: Upload Chocolatey package + uses: actions/upload-artifact@v4 + with: + name: chocolatey-package-${{ steps.get_version.outputs.version }} + path: chocolatey/*.nupkg + retention-days: 90 + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.get_version.outputs.version }} + files: chocolatey/*.nupkg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.gitignore b/.gitignore index be2f63d..dd4c524 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ artifacts/ # NuGet Packages *.nupkg *.snupkg -**/packages/* \ No newline at end of file +**/packages/* + +dist/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f9366..392330f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog -All notable changes to DDCSwitch will be documented in this file. +All notable changes to ddcswitch will be documented in this file. + +## [1.0.2] - 2026-01-07 + +### Added +- Full support for all DDC/CI compliant monitors +- Improved error handling for unsupported monitors +- Enhanced documentation with additional examples +- Optimized performance for input switching operations +- Better clarity in CLI output messages ## [1.0.1] - 2026-01-07 diff --git a/DDCSwitch/Commands/CommandRouter.cs b/DDCSwitch/Commands/CommandRouter.cs new file mode 100644 index 0000000..e9761c3 --- /dev/null +++ b/DDCSwitch/Commands/CommandRouter.cs @@ -0,0 +1,77 @@ +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class CommandRouter +{ + public static int Route(string[] args) + { + if (args.Length == 0) + { + HelpCommand.ShowUsage(); + return 1; + } + + // Check for --json flag + bool jsonOutput = args.Contains("--json", StringComparer.OrdinalIgnoreCase); + var filteredArgs = args.Where(a => !a.Equals("--json", StringComparison.OrdinalIgnoreCase)).ToArray(); + + // Check for --verbose flag + bool verboseOutput = filteredArgs.Contains("--verbose", StringComparer.OrdinalIgnoreCase); + filteredArgs = filteredArgs.Where(a => !a.Equals("--verbose", StringComparison.OrdinalIgnoreCase)).ToArray(); + + + + if (filteredArgs.Length == 0) + { + HelpCommand.ShowUsage(); + return 1; + } + + var command = filteredArgs[0].ToLowerInvariant(); + + try + { + return command switch + { + "list" or "ls" => ListCommand.Execute(jsonOutput, verboseOutput), + "get" => GetCommand.Execute(filteredArgs, jsonOutput), + "set" => SetCommand.Execute(filteredArgs, jsonOutput), + "version" or "-v" or "--version" => HelpCommand.ShowVersion(jsonOutput), + "help" or "-h" or "--help" or "/?" => HelpCommand.ShowUsage(), + _ => InvalidCommand(filteredArgs[0], jsonOutput) + }; + } + catch (Exception 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; + } + } + + private static int InvalidCommand(string command, bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, $"Unknown command: {command}"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Unknown command: {command}"); + ConsoleOutputFormatter.WriteInfo("Run ddcswitch help for usage information."); + } + + return 1; + } +} + diff --git a/DDCSwitch/Commands/ConsoleOutputFormatter.cs b/DDCSwitch/Commands/ConsoleOutputFormatter.cs new file mode 100644 index 0000000..3a917f2 --- /dev/null +++ b/DDCSwitch/Commands/ConsoleOutputFormatter.cs @@ -0,0 +1,42 @@ +using Spectre.Console; + +namespace DDCSwitch.Commands; + +internal static class ConsoleOutputFormatter +{ + public static void WriteError(string message) + { + AnsiConsole.MarkupLine($"[bold red]X Error:[/] [red]{message}[/]"); + } + + public static void WriteInfo(string message) + { + AnsiConsole.MarkupLine($"[cyan]i[/] {message}"); + } + + public static void WriteSuccess(string message) + { + AnsiConsole.MarkupLine($"[bold green]> Success:[/] [green]{message}[/]"); + } + + public static void WriteWarning(string message) + { + AnsiConsole.MarkupLine($"[bold yellow]! Warning:[/] [yellow]{message}[/]"); + } + + public static void WriteHeader(string text) + { + var rule = new Rule($"[bold cyan]{text}[/]") + { + Justification = Justify.Left + }; + AnsiConsole.Write(rule); + } + + public static void WriteMonitorInfo(string label, string value, bool highlight = false) + { + var color = highlight ? "yellow" : "cyan"; + AnsiConsole.MarkupLine($" [bold {color}]{label}:[/] {value}"); + } +} + diff --git a/DDCSwitch/Commands/GetCommand.cs b/DDCSwitch/Commands/GetCommand.cs new file mode 100644 index 0000000..7c6914d --- /dev/null +++ b/DDCSwitch/Commands/GetCommand.cs @@ -0,0 +1,307 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class GetCommand +{ + 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 get [feature]"); + AnsiConsole.WriteLine(" ddcswitch get all"); + } + + return 1; + } + + // Check if the monitor identifier is "all" + if (args[1].Equals("all", StringComparison.OrdinalIgnoreCase)) + { + // "get all" should only scan all monitors, no specific feature + if (args.Length > 2) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, "Feature specification not supported with 'get all'"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError("Feature specification not supported with 'get all'."); + AnsiConsole.WriteLine("Usage: ddcswitch get all"); + } + return 1; + } + + return GetAllMonitors(jsonOutput); + } + + // If no feature is specified, perform VCP scan + if (args.Length == 2) + { + return VcpScanCommand.ScanSingleMonitor(args[1], jsonOutput); + } + + string featureInput = args[2]; + + if (!FeatureResolver.TryResolveFeature(featureInput, out VcpFeature? feature)) + { + return HandleInvalidFeature(featureInput, jsonOutput); + } + + var 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 = ReadFeatureValue(monitor, feature!, jsonOutput); + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static int GetAllMonitors(bool jsonOutput) + { + 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) + { + 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; + } + + return VcpScanCommand.ScanAllMonitors(monitors, jsonOutput); + } + + private static int HandleInvalidFeature(string featureInput, bool jsonOutput) + { + string errorMessage; + + // Provide specific error message based on input type + if (FeatureResolver.TryParseVcpCode(featureInput, out _)) + { + // Valid VCP code but not in our predefined list + errorMessage = $"VCP code '{featureInput}' is valid but may not be supported by all monitors"; + } + else + { + // Invalid feature name or VCP code + errorMessage = $"Invalid feature '{featureInput}'. {FeatureResolver.GetVcpCodeValidationError(featureInput)}"; + } + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine("Valid features: brightness, contrast, input, or VCP code (0x10, 0x12, etc.)"); + } + + 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 monitorId, bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, $"Monitor '{monitorId}' not found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Monitor '{monitorId}' not found."); + AnsiConsole.MarkupLine("Use [yellow]ddcswitch list[/] to see available monitors."); + } + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } + + private static int ReadFeatureValue(Monitor monitor, VcpFeature feature, bool jsonOutput) + { + bool success = false; + uint current = 0; + uint max = 0; + int errorCode = 0; + + if (!jsonOutput) + { + AnsiConsole.Status() + .Start($"Reading {feature.Name} from {monitor.Name}...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + success = monitor.TryGetVcpFeature(feature.Code, out current, out max, out errorCode); + }); + } + else + { + success = monitor.TryGetVcpFeature(feature.Code, out current, out max, out errorCode); + } + + if (!success) + { + return HandleReadFailure(monitor, feature, errorCode, jsonOutput); + } + + OutputFeatureValue(monitor, feature, current, max, jsonOutput); + return 0; + } + + private static int HandleReadFailure(Monitor monitor, VcpFeature feature, int errorCode, bool jsonOutput) + { + string errorMessage; + + if (VcpErrorHandler.IsTimeoutError(errorCode)) + { + errorMessage = VcpErrorHandler.CreateTimeoutMessage(monitor, feature, "read"); + } + else if (VcpErrorHandler.IsUnsupportedFeatureError(errorCode)) + { + errorMessage = VcpErrorHandler.CreateUnsupportedFeatureMessage(monitor, feature); + } + else if (errorCode == 0x00000006) // ERROR_INVALID_HANDLE + { + errorMessage = VcpErrorHandler.CreateCommunicationFailureMessage(monitor); + } + else + { + errorMessage = VcpErrorHandler.CreateReadFailureMessage(monitor, feature); + } + + 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; + } + + private static void OutputFeatureValue(Monitor monitor, VcpFeature feature, uint current, uint max, bool jsonOutput) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + + uint? percentageValue = feature.SupportsPercentage ? FeatureResolver.ConvertRawToPercentage(current, max) : null; + var result = new GetVcpResponse(true, monitorRef, feature.Name, current, max, percentageValue); + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.GetVcpResponse)); + } + else + { + var panel = new Panel( + $"[bold cyan]Monitor:[/] {monitor.Name}\n" + + $"[dim]Device:[/] [dim]{monitor.DeviceName}[/]") + { + Header = new PanelHeader($"[bold green]>> Feature Value[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Cyan) + }; + AnsiConsole.Write(panel); + + 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})[/]"); + } + else if (feature.SupportsPercentage) + { + // Display percentage for brightness/contrast + uint percentage = FeatureResolver.ConvertRawToPercentage(current, max); + var progressBar = new BarChart() + .Width(40) + .Label($"[bold yellow]{feature.Name}[/]") + .CenterLabel() + .AddItem("", percentage, Color.Green); + + AnsiConsole.Write(progressBar); + AnsiConsole.MarkupLine($" [bold green]{percentage}%[/] [dim](raw: {current}/{max})[/]"); + } + else + { + // Display raw values for unknown VCP codes + AnsiConsole.MarkupLine($" [bold yellow]{feature.Name}:[/] [green]{current}[/] [dim](max: {max})[/]"); + } + } + } +} + diff --git a/DDCSwitch/Commands/HelpCommand.cs b/DDCSwitch/Commands/HelpCommand.cs new file mode 100644 index 0000000..54895b0 --- /dev/null +++ b/DDCSwitch/Commands/HelpCommand.cs @@ -0,0 +1,97 @@ +using Spectre.Console; + +namespace DDCSwitch.Commands; + +internal static class HelpCommand +{ + public static string GetVersion() + { + var version = typeof(HelpCommand).Assembly + .GetName().Version?.ToString(3) ?? "0.0.0"; + return version; + } + + public static int ShowVersion(bool jsonOutput) + { + var version = GetVersion(); + + if (jsonOutput) + { + Console.WriteLine($"{{\"version\":\"{version}\"}}"); + } + else + { + AnsiConsole.Write(new FigletText("ddcswitch").Color(Color.Cyan1)); + AnsiConsole.MarkupLine($"[bold white] v{version}[/]"); + AnsiConsole.MarkupLine("[dim italic]>> Windows DDC/CI Monitor Input Switcher[/]"); + } + + return 0; + } + + public static int ShowUsage() + { + var version = GetVersion(); + + AnsiConsole.Write(new FigletText("ddcswitch").Color(Color.Cyan1)); + AnsiConsole.MarkupLine($"[bold white]v{version}[/]"); + AnsiConsole.MarkupLine($"[dim italic]>> A Windows command-line utility to control monitors using DDC/CI[/]\n"); + + var commandsTable = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.White) + .AddColumn(new TableColumn("[bold yellow]Command[/]").LeftAligned()) + .AddColumn(new TableColumn("[bold yellow]Description[/]").LeftAligned()); + + commandsTable.AddRow( + "[cyan]list[/] [dim]or[/] [cyan]ls[/]", + "List all DDC/CI capable monitors with current input sources"); + commandsTable.AddRow( + "[cyan]list --verbose[/]", + "Include brightness and contrast information"); + commandsTable.AddRow( + "[cyan]get all[/]", + "Enumerate all supported VCP features for all monitors"); + commandsTable.AddRow( + "[cyan]get[/] [green][/] [blue][[feature]][/]", + "Get current value for a monitor feature or get all features"); + commandsTable.AddRow( + "[cyan]set[/] [green][/] [blue][/] [magenta][/]", + "Set value for a monitor feature"); + commandsTable.AddRow( + "[cyan]version[/] [dim]or[/] [cyan]-v[/]", + "Display version information"); + commandsTable.AddRow( + "[cyan]help[/] [dim]or[/] [cyan]-h[/]", + "Show this help message"); + + AnsiConsole.Write(commandsTable); + + AnsiConsole.WriteLine(); + + var panel = new Panel( + "[bold yellow]Features:[/] brightness, contrast, input, or VCP codes like [cyan]0x10[/]\n" + + "[bold yellow]Flags:[/]\n" + + " [cyan]--json[/] Machine-readable JSON output for automation\n" + + " [cyan]--verbose[/] Include detailed information in list command") + { + Header = new PanelHeader("[bold green]>> Quick Reference[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.White) + }; + + AnsiConsole.Write(panel); + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[dim]Examples:[/]"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch list"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch get all"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch get 0"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch get 0 brightness"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 0 input HDMI1"); + AnsiConsole.MarkupLine(" [grey]$[/] ddcswitch set 1 brightness 75%"); + + return 0; + } +} + diff --git a/DDCSwitch/Commands/ListCommand.cs b/DDCSwitch/Commands/ListCommand.cs new file mode 100644 index 0000000..87cb204 --- /dev/null +++ b/DDCSwitch/Commands/ListCommand.cs @@ -0,0 +1,237 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class ListCommand +{ + public static int Execute(bool jsonOutput, bool verboseOutput) + { + 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) + { + if (jsonOutput) + { + var result = new ListMonitorsResponse(false, null, "No DDC/CI capable monitors found"); + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.ListMonitorsResponse)); + } + else + { + AnsiConsole.MarkupLine("[yellow]No DDC/CI capable monitors found.[/]"); + } + + return 1; + } + + if (jsonOutput) + { + OutputJsonList(monitors, verboseOutput); + } + else + { + OutputTableList(monitors, verboseOutput); + } + + // Cleanup + foreach (var monitor in monitors) + { + monitor.Dispose(); + } + + return 0; + } + + private static void OutputJsonList(List monitors, bool verboseOutput) + { + var monitorList = monitors.Select(monitor => + { + string? inputName = null; + uint? inputCode = null; + string status = "ok"; + string? brightness = null; + string? contrast = null; + + try + { + if (monitor.TryGetInputSource(out uint current, out _)) + { + inputName = InputSource.GetName(current); + inputCode = current; + } + else + { + status = "no_ddc_ci"; + } + + // Get brightness and contrast if verbose mode is enabled + if (verboseOutput && status == "ok") + { + // Try to get brightness (VCP 0x10) + if (monitor.TryGetVcpFeature(VcpFeature.Brightness.Code, out uint brightnessCurrent, out uint brightnessMax)) + { + uint brightnessPercentage = FeatureResolver.ConvertRawToPercentage(brightnessCurrent, brightnessMax); + brightness = $"{brightnessPercentage}%"; + } + else + { + brightness = "N/A"; + } + + // Try to get contrast (VCP 0x12) + if (monitor.TryGetVcpFeature(VcpFeature.Contrast.Code, out uint contrastCurrent, out uint contrastMax)) + { + uint contrastPercentage = FeatureResolver.ConvertRawToPercentage(contrastCurrent, contrastMax); + contrast = $"{contrastPercentage}%"; + } + else + { + contrast = "N/A"; + } + } + } + catch + { + status = "error"; + if (verboseOutput) + { + brightness = "N/A"; + contrast = "N/A"; + } + } + + return new MonitorInfo( + monitor.Index, + monitor.Name, + monitor.DeviceName, + monitor.IsPrimary, + inputName, + inputCode != null ? $"0x{inputCode:X2}" : null, + status, + brightness, + contrast); + }).ToList(); + + var result = new ListMonitorsResponse(true, monitorList); + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.ListMonitorsResponse)); + } + + private static void OutputTableList(List monitors, bool verboseOutput) + { + var table = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.White) + .AddColumn(new TableColumn("[bold yellow]Index[/]").Centered()) + .AddColumn(new TableColumn("[bold yellow]Monitor Name[/]").LeftAligned()) + .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 + if (verboseOutput) + { + table.AddColumn(new TableColumn("[bold yellow]Brightness[/]").Centered()); + table.AddColumn(new TableColumn("[bold yellow]Contrast[/]").Centered()); + } + + table.AddColumn(new TableColumn("[bold yellow]Status[/]").Centered()); + + foreach (var monitor in monitors) + { + string inputInfo = "[dim]N/A[/]"; + string status = "[green]+[/] [bold green]OK[/]"; + string brightnessInfo = "[dim]N/A[/]"; + string contrastInfo = "[dim]N/A[/]"; + + try + { + if (monitor.TryGetInputSource(out uint current, out _)) + { + var inputName = InputSource.GetName(current); + inputInfo = $"[cyan]{inputName}[/] [dim](0x{current:X2})[/]"; + } + else + { + 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[/]") + { + // Try to get brightness (VCP 0x10) + if (monitor.TryGetVcpFeature(VcpFeature.Brightness.Code, out uint brightnessCurrent, out uint brightnessMax)) + { + uint brightnessPercentage = FeatureResolver.ConvertRawToPercentage(brightnessCurrent, brightnessMax); + brightnessInfo = $"[green]{brightnessPercentage}%[/]"; + } + else + { + brightnessInfo = "[dim]N/A[/]"; + } + + // Try to get contrast (VCP 0x12) + if (monitor.TryGetVcpFeature(VcpFeature.Contrast.Code, out uint contrastCurrent, out uint contrastMax)) + { + uint contrastPercentage = FeatureResolver.ConvertRawToPercentage(contrastCurrent, contrastMax); + contrastInfo = $"[green]{contrastPercentage}%[/]"; + } + else + { + contrastInfo = "[dim]N/A[/]"; + } + } + else if (verboseOutput) + { + brightnessInfo = "[dim]N/A[/]"; + contrastInfo = "[dim]N/A[/]"; + } + } + catch + { + status = "[red]X[/] [bold red]Error[/]"; + if (verboseOutput) + { + brightnessInfo = "[dim]N/A[/]"; + contrastInfo = "[dim]N/A[/]"; + } + } + + var row = new List + { + monitor.IsPrimary ? $"[bold cyan]{monitor.Index}[/] [yellow]*[/]" : $"[cyan]{monitor.Index}[/]", + monitor.Name, + $"[dim]{monitor.DeviceName}[/]", + inputInfo + }; + + // Add brightness and contrast columns if verbose mode is enabled + if (verboseOutput) + { + row.Add(brightnessInfo); + row.Add(contrastInfo); + } + + row.Add(status); + + table.AddRow(row.ToArray()); + } + + AnsiConsole.Write(table); + } +} + diff --git a/DDCSwitch/Commands/SetCommand.cs b/DDCSwitch/Commands/SetCommand.cs new file mode 100644 index 0000000..ce4b6c2 --- /dev/null +++ b/DDCSwitch/Commands/SetCommand.cs @@ -0,0 +1,402 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class SetCommand +{ + public static int Execute(string[] args, bool jsonOutput) + { + // Require 4 arguments: set + if (args.Length < 4) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, "Monitor, feature, and value required"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError("Monitor, feature, and value required."); + AnsiConsole.MarkupLine("Usage: [yellow]ddcswitch set [/]"); + } + + return 1; + } + + string featureInput = args[2]; + string valueInput = args[3]; + + if (!FeatureResolver.TryResolveFeature(featureInput, out VcpFeature? feature)) + { + return HandleInvalidFeature(featureInput, jsonOutput); + } + + var (setValue, percentageValue, validationError) = ParseAndValidateValue(feature!, valueInput); + + if (validationError != null) + { + return HandleValidationError(validationError, jsonOutput); + } + + var 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 = SetFeatureValue(monitor, feature!, setValue, percentageValue, jsonOutput); + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static int HandleInvalidFeature(string featureInput, bool jsonOutput) + { + string errorMessage; + + // Provide specific error message based on input type + if (FeatureResolver.TryParseVcpCode(featureInput, out _)) + { + // Valid VCP code but not in our predefined list + errorMessage = $"VCP code '{featureInput}' is valid but may not be supported by all monitors"; + } + else + { + // Invalid feature name or VCP code + errorMessage = $"Invalid feature '{featureInput}'. {FeatureResolver.GetVcpCodeValidationError(featureInput)}"; + } + + if (jsonOutput) + { + var error = new ErrorResponse(false, errorMessage); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMessage); + AnsiConsole.MarkupLine("Valid features: brightness, contrast, input, or VCP code (0x10, 0x12, etc.)"); + } + + return 1; + } + + private static (uint setValue, uint? percentageValue, string? validationError) ParseAndValidateValue(VcpFeature feature, string valueInput) + { + uint setValue = 0; + uint? percentageValue = null; + string? validationError = null; + + if (feature.Code == InputSource.VcpInputSource) + { + // Use existing input source parsing for input feature + if (!InputSource.TryParse(valueInput, out setValue)) + { + validationError = $"Invalid input source '{valueInput}'. Valid inputs: HDMI1, HDMI2, DP1, DP2, DVI1, DVI2, VGA1, VGA2, or hex code (0x11)"; + } + } + else if (feature.SupportsPercentage && FeatureResolver.TryParsePercentage(valueInput, out uint percentage)) + { + // Parse as percentage for brightness/contrast - validate percentage range + if (!FeatureResolver.IsValidPercentage(percentage)) + { + validationError = VcpErrorHandler.CreateRangeValidationMessage(feature, percentage, 100, true); + } + else + { + percentageValue = percentage; + // We'll convert to raw value after getting monitor's max value + setValue = 0; // Placeholder + } + } + else if (uint.TryParse(valueInput, out uint rawValue)) + { + // Parse as raw value - we'll validate range after getting monitor's max value + setValue = rawValue; + } + else + { + // Invalid value format + if (feature.SupportsPercentage) + { + validationError = FeatureResolver.GetPercentageValidationError(valueInput); + } + else + { + validationError = $"Invalid value '{valueInput}' for feature '{feature.Name}'. Expected: numeric value within monitor's supported range"; + } + } + + return (setValue, percentageValue, validationError); + } + + private static int HandleValidationError(string validationError, bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, validationError); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(validationError); + } + + 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 monitorId, bool jsonOutput) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, $"Monitor '{monitorId}' not found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Monitor '{monitorId}' not found."); + AnsiConsole.MarkupLine("Use [yellow]ddcswitch list[/] to see available monitors."); + } + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } + + private static int SetFeatureValue(Monitor monitor, VcpFeature feature, uint setValue, uint? percentageValue, bool jsonOutput) + { + // If we have a percentage value or need to validate raw value range, get the monitor's max value + if (percentageValue.HasValue || (feature.Code != InputSource.VcpInputSource && !percentageValue.HasValue)) + { + if (monitor.TryGetVcpFeature(feature.Code, out _, out uint maxValue, out int errorCode)) + { + if (percentageValue.HasValue) + { + // Convert percentage to raw value + setValue = FeatureResolver.ConvertPercentageToRaw(percentageValue.Value, maxValue); + } + else if (feature.Code != InputSource.VcpInputSource) + { + // Validate raw value is within supported range + if (!FeatureResolver.IsValidRawVcpValue(setValue, maxValue)) + { + string rangeError = VcpErrorHandler.CreateRangeValidationMessage(feature, setValue, maxValue); + + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, rangeError, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(rangeError); + } + + return 1; + } + } + } + else + { + return HandleReadErrorDuringValidation(monitor, feature, errorCode, jsonOutput); + } + } + + return WriteFeatureValue(monitor, feature, setValue, percentageValue, jsonOutput); + } + + private static int HandleReadErrorDuringValidation(Monitor monitor, VcpFeature feature, int errorCode, bool jsonOutput) + { + string readError; + + if (VcpErrorHandler.IsTimeoutError(errorCode)) + { + readError = VcpErrorHandler.CreateTimeoutMessage(monitor, feature, "read"); + } + else if (VcpErrorHandler.IsUnsupportedFeatureError(errorCode)) + { + readError = VcpErrorHandler.CreateUnsupportedFeatureMessage(monitor, feature); + } + else if (errorCode == 0x00000006) // ERROR_INVALID_HANDLE + { + readError = VcpErrorHandler.CreateCommunicationFailureMessage(monitor); + } + else + { + readError = $"Failed to read current {feature.Name} from monitor '{monitor.Name}' to validate range. {VcpErrorHandler.CreateReadFailureMessage(monitor, feature)}"; + } + + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, readError, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(readError); + } + + return 1; + } + + private static int WriteFeatureValue(Monitor monitor, VcpFeature feature, uint setValue, uint? percentageValue, bool jsonOutput) + { + bool success = false; + string? errorMsg = null; + + if (!jsonOutput) + { + string displayValue = percentageValue.HasValue ? $"{percentageValue}%" : setValue.ToString(); + AnsiConsole.Status() + .Start($"Setting {monitor.Name} {feature.Name} to {displayValue}...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + + if (!monitor.TrySetVcpFeature(feature.Code, setValue, out int errorCode)) + { + errorMsg = GetWriteErrorMessage(monitor, feature, setValue, errorCode); + } + else + { + success = true; + } + + if (success) + { + // Give the monitor a moment to apply the change + Thread.Sleep(500); + } + }); + } + else + { + if (!monitor.TrySetVcpFeature(feature.Code, setValue, out int errorCode)) + { + errorMsg = GetWriteErrorMessage(monitor, feature, setValue, errorCode); + } + else + { + success = true; + Thread.Sleep(500); + } + } + + if (!success) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var error = new ErrorResponse(false, errorMsg!, monitorRef); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError(errorMsg!); + } + + return 1; + } + + OutputSuccess(monitor, feature, setValue, percentageValue, jsonOutput); + return 0; + } + + private static string GetWriteErrorMessage(Monitor monitor, VcpFeature feature, uint setValue, int errorCode) + { + if (VcpErrorHandler.IsTimeoutError(errorCode)) + { + return VcpErrorHandler.CreateTimeoutMessage(monitor, feature, "write"); + } + else if (VcpErrorHandler.IsUnsupportedFeatureError(errorCode)) + { + return VcpErrorHandler.CreateUnsupportedFeatureMessage(monitor, feature); + } + else if (errorCode == 0x00000006) // ERROR_INVALID_HANDLE + { + return VcpErrorHandler.CreateCommunicationFailureMessage(monitor); + } + else + { + return VcpErrorHandler.CreateWriteFailureMessage(monitor, feature, setValue); + } + } + + private static void OutputSuccess(Monitor monitor, VcpFeature feature, uint setValue, uint? percentageValue, bool jsonOutput) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + + // Use generic VCP response for all features + var result = new SetVcpResponse(true, monitorRef, feature.Name, setValue, percentageValue); + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.SetVcpResponse)); + } + else + { + string displayValue; + if (feature.Code == InputSource.VcpInputSource) + { + // Display input with name resolution + displayValue = $"[cyan]{InputSource.GetName(setValue)}[/]"; + } + else if (percentageValue.HasValue) + { + // Display percentage for brightness/contrast + displayValue = $"[green]{percentageValue}%[/]"; + } + else + { + // Display raw value for unknown VCP codes + displayValue = $"[green]{setValue}[/]"; + } + + var successPanel = new Panel( + $"[bold cyan]Monitor:[/] {monitor.Name}\n" + + $"[bold yellow]Feature:[/] {feature.Name}\n" + + $"[bold green]New Value:[/] {displayValue}") + { + Header = new PanelHeader("[bold green]>> Successfully Applied[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Green) + }; + + AnsiConsole.Write(successPanel); + } + } +} + diff --git a/DDCSwitch/Commands/VcpScanCommand.cs b/DDCSwitch/Commands/VcpScanCommand.cs new file mode 100644 index 0000000..971465e --- /dev/null +++ b/DDCSwitch/Commands/VcpScanCommand.cs @@ -0,0 +1,299 @@ +using Spectre.Console; +using System.Text.Json; + +namespace DDCSwitch.Commands; + +internal static class VcpScanCommand +{ + public static int ScanAllMonitors(List monitors, bool jsonOutput) + { + if (jsonOutput) + { + OutputJsonScanAll(monitors); + } + else + { + OutputTableScanAll(monitors); + } + + // Cleanup + foreach (var monitor in monitors) + { + monitor.Dispose(); + } + + return 0; + } + + public static int ScanSingleMonitor(string monitorIdentifier, bool jsonOutput) + { + 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) + { + 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; + } + + var monitor = MonitorController.FindMonitor(monitors, monitorIdentifier); + + if (monitor == null) + { + if (jsonOutput) + { + var error = new ErrorResponse(false, $"Monitor '{monitorIdentifier}' not found"); + Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Monitor '{monitorIdentifier}' not found."); + AnsiConsole.MarkupLine("Use [yellow]ddcswitch list[/] to see available monitors."); + } + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return 1; + } + + int result; + + try + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + Dictionary features; + + if (!jsonOutput) + { + features = null!; + AnsiConsole.Status() + .Start($"Scanning VCP features for {monitor.Name}...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + features = monitor.ScanVcpFeatures(); + }); + } + else + { + features = monitor.ScanVcpFeatures(); + } + + // Filter only supported features for cleaner output + var supportedFeatures = features.Values + .Where(f => f.IsSupported) + .OrderBy(f => f.Code) + .ToList(); + + if (jsonOutput) + { + OutputJsonScanSingle(monitorRef, supportedFeatures); + } + else + { + OutputTableScanSingle(monitor, supportedFeatures); + } + + result = 0; + } + catch (Exception ex) + { + if (jsonOutput) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var scanResult = new VcpScanResponse(false, monitorRef, new List(), ex.Message); + Console.WriteLine(JsonSerializer.Serialize(scanResult, JsonContext.Default.VcpScanResponse)); + } + else + { + ConsoleOutputFormatter.WriteError($"Error scanning monitor {monitor.Index} ({monitor.Name}): {ex.Message}"); + } + + result = 1; + } + + // Cleanup + foreach (var m in monitors) + { + m.Dispose(); + } + + return result; + } + + private static void OutputJsonScanAll(List monitors) + { + var scanResults = new List(); + + foreach (var monitor in monitors) + { + try + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + var features = monitor.ScanVcpFeatures(); + + // Convert to list and filter only supported features for cleaner output + var supportedFeatures = features.Values + .Where(f => f.IsSupported) + .OrderBy(f => f.Code) + .ToList(); + + scanResults.Add(new VcpScanResponse(true, monitorRef, supportedFeatures)); + } + catch (Exception ex) + { + var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); + scanResults.Add(new VcpScanResponse(false, monitorRef, new List(), ex.Message)); + } + } + + // Output all scan results + foreach (var result in scanResults) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.VcpScanResponse)); + } + } + + private static void OutputTableScanAll(List monitors) + { + foreach (var monitor in monitors) + { + try + { + var rule = new Rule($"[bold cyan]Monitor {monitor.Index}: {monitor.Name}[/] [dim]({monitor.DeviceName})[/]") + { + Justification = Justify.Left, + Style = new Style(Color.Cyan) + }; + AnsiConsole.Write(rule); + + Dictionary features = null!; + AnsiConsole.Status() + .Start($"Scanning VCP features for {monitor.Name}...", ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("cyan")); + features = monitor.ScanVcpFeatures(); + }); + + var supportedFeatures = features.Values + .Where(f => f.IsSupported) + .OrderBy(f => f.Code) + .ToList(); + + if (supportedFeatures.Count == 0) + { + ConsoleOutputFormatter.WriteWarning("No supported VCP features found"); + AnsiConsole.WriteLine(); + continue; + } + + AnsiConsole.MarkupLine($"[bold green]>> Found {supportedFeatures.Count} supported features[/]\n"); + OutputFeatureTable(supportedFeatures); + AnsiConsole.WriteLine(); + } + catch (Exception ex) + { + ConsoleOutputFormatter.WriteError($"Error scanning monitor {monitor.Index} ({monitor.Name}): {ex.Message}"); + AnsiConsole.WriteLine(); + } + } + } + + private static void OutputJsonScanSingle(MonitorReference monitorRef, List supportedFeatures) + { + var scanResult = new VcpScanResponse(true, monitorRef, supportedFeatures); + Console.WriteLine(JsonSerializer.Serialize(scanResult, JsonContext.Default.VcpScanResponse)); + } + + private static void OutputTableScanSingle(Monitor monitor, 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}[/]") + { + Header = new PanelHeader($"[bold cyan]>> VCP Feature Scan Results[/]", Justify.Left), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.White) + }; + AnsiConsole.Write(panel); + + if (supportedFeatures.Count == 0) + { + ConsoleOutputFormatter.WriteWarning("No supported VCP features found"); + } + else + { + OutputFeatureTable(supportedFeatures); + } + } + + private static void OutputFeatureTable(List supportedFeatures) + { + var table = new Table() + .Border(TableBorder.Rounded) + .BorderColor(Color.White) + .AddColumn(new TableColumn("[bold yellow]VCP Code[/]").Centered()) + .AddColumn(new TableColumn("[bold yellow]Feature Name[/]").LeftAligned()) + .AddColumn(new TableColumn("[bold yellow]Access[/]").Centered()) + .AddColumn(new TableColumn("[bold yellow]Current[/]").RightAligned()) + .AddColumn(new TableColumn("[bold yellow]Max[/]").RightAligned()); + + foreach (var feature in 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]? ?[/]" + }; + + string currentValue = $"[green]{feature.CurrentValue}[/]"; + string maxValue = $"[cyan]{feature.MaxValue}[/]"; + + var name = feature.Name; + + // If the feature name isn't known, make it dim + if (feature.Name.StartsWith("VCP_")) + { + name = $"[dim]{feature.Name}[/]"; + } + + table.AddRow(vcpCode, name, accessType, currentValue, maxValue); + } + + AnsiConsole.Write(table); + } +} + diff --git a/DDCSwitch/DDCSwitch.csproj b/DDCSwitch/DDCSwitch.csproj index 04e5d93..c9d1921 100644 --- a/DDCSwitch/DDCSwitch.csproj +++ b/DDCSwitch/DDCSwitch.csproj @@ -11,6 +11,7 @@ Speed false true + ddcswitch @@ -19,7 +20,7 @@ $(MSBuildProjectDirectory)\..\CHANGELOG.md - + @(ChangelogLines, '%0A') @@ -33,12 +34,24 @@ $(Version) $(Version) - + + + + + + + + True + True + VcpFeatureData.json + + + diff --git a/DDCSwitch/FeatureResolver.cs b/DDCSwitch/FeatureResolver.cs new file mode 100644 index 0000000..ffb3f51 --- /dev/null +++ b/DDCSwitch/FeatureResolver.cs @@ -0,0 +1,382 @@ +using System.Globalization; + +namespace DDCSwitch; + +/// +/// Resolves feature names to VCP codes and handles value conversions +/// +public static class FeatureResolver +{ + private static readonly Dictionary FeatureMap = BuildFeatureMap(); + private static readonly Dictionary CodeMap = BuildCodeMap(); + + /// + /// Builds the feature name to VcpFeature mapping including aliases + /// + private static Dictionary BuildFeatureMap() + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var feature in VcpFeature.AllFeatures) + { + // Add primary name + map[feature.Name] = feature; + + // Add aliases + foreach (var alias in feature.Aliases) + { + map[alias] = feature; + } + } + + return map; + } + + /// + /// Builds the VCP code to VcpFeature mapping + /// + private static Dictionary BuildCodeMap() + { + var map = new Dictionary(); + + foreach (var feature in VcpFeature.AllFeatures) + { + map[feature.Code] = feature; + } + + return map; + } + + /// + /// Attempts to resolve a feature name or VCP code to a VcpFeature + /// + /// Feature name (brightness, contrast, input) or VCP code (0x10, 0x12, etc.) + /// The resolved VcpFeature if successful + /// True if the feature was resolved successfully + public static bool TryResolveFeature(string input, out VcpFeature? feature) + { + feature = null; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + // First try to resolve as a known feature name or alias + if (FeatureMap.TryGetValue(input.Trim(), out feature)) + { + return true; + } + + // Try to parse as a VCP code + if (TryParseVcpCode(input, out byte vcpCode)) + { + // Check if we have a predefined feature for this code + if (CodeMap.TryGetValue(vcpCode, out feature)) + { + return true; + } + + // Create a generic VCP feature for unknown codes + feature = new VcpFeature(vcpCode, $"VCP_{vcpCode:X2}", $"Unknown VCP feature 0x{vcpCode:X2}", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + return true; + } + + return false; + } + + /// + /// Gets VCP features by category + /// + /// The category to filter by + /// Array of VCP features in the specified category + public static VcpFeature[] GetFeaturesByCategory(VcpFeatureCategory category) + { + return VcpFeature.AllFeatures.Where(f => f.Category == category).ToArray(); + } + + /// + /// Searches for VCP features by partial name matching + /// + /// Partial name to search for + /// Array of VCP features matching the partial name + public static VcpFeature[] SearchFeatures(string partialName) + { + if (string.IsNullOrWhiteSpace(partialName)) + { + return Array.Empty(); + } + + var searchTerm = partialName.Trim(); + return VcpFeature.AllFeatures + .Where(f => f.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + f.Description.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || + f.Aliases.Any(alias => alias.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))) + .ToArray(); + } + + /// + /// Gets a VCP feature by its code + /// + /// VCP code + /// VCP feature if found, otherwise a generic feature for the code + public static VcpFeature GetFeatureByCode(byte code) + { + if (CodeMap.TryGetValue(code, out var feature)) + { + return feature; + } + + // Return a generic feature for unknown codes + return new VcpFeature(code, $"VCP_{code:X2}", $"Unknown VCP feature 0x{code:X2}", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + } + + /// + /// Gets all available VCP feature categories + /// + /// Array of category names + public static string[] GetAllCategories() + { + return Enum.GetNames(); + } + + /// + /// Converts a percentage value (0-100) to raw VCP value based on the maximum value + /// + /// Percentage value (0-100) + /// Maximum raw value supported by the monitor + /// Raw VCP value + public static uint ConvertPercentageToRaw(uint percentage, uint maxValue) + { + if (percentage > 100) + { + throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be between 0 and 100"); + } + + if (maxValue == 0) + { + return 0; + } + + // Convert percentage to raw value with proper rounding + return (uint)Math.Round((double)percentage * maxValue / 100.0); + } + + /// + /// Converts a raw VCP value to percentage based on the maximum value + /// + /// Raw VCP value + /// Maximum raw value supported by the monitor + /// Percentage value (0-100) + public static uint ConvertRawToPercentage(uint rawValue, uint maxValue) + { + if (maxValue == 0) + { + return 0; + } + + if (rawValue > maxValue) + { + rawValue = maxValue; + } + + // Convert raw value to percentage with proper rounding + return (uint)Math.Round((double)rawValue * 100.0 / maxValue); + } + + /// + /// Attempts to parse a VCP code from a string (supports hex format like 0x10 or decimal) + /// + /// Input string containing VCP code + /// Parsed VCP code if successful + /// True if parsing was successful and VCP code is in valid range (0x00-0xFF) + public static bool TryParseVcpCode(string input, out byte vcpCode) + { + vcpCode = 0; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + input = input.Trim(); + + // Try hex format (0x10, 0X10) + if (input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || + input.StartsWith("0X", StringComparison.OrdinalIgnoreCase)) + { + var hexPart = input.Substring(2); + if (byte.TryParse(hexPart, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out vcpCode)) + { + // VCP codes are inherently valid for byte range (0x00-0xFF) + return true; + } + return false; + } + + // Try decimal format - validate range for decimal input + if (uint.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint decimalValue)) + { + // Validate VCP code is in valid range (0x00-0xFF) + if (decimalValue <= 255) + { + vcpCode = (byte)decimalValue; + return true; + } + } + + return false; + } + + /// + /// Attempts to parse a percentage value from a string (supports % suffix) + /// + /// Input string containing percentage value + /// Parsed percentage value if successful + /// True if parsing was successful and value is in valid range (0-100) + public static bool TryParsePercentage(string input, out uint percentage) + { + percentage = 0; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + input = input.Trim(); + + // Remove % suffix if present + if (input.EndsWith("%")) + { + input = input.Substring(0, input.Length - 1).Trim(); + } + + if (!uint.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out percentage)) + { + return false; + } + + // Validate range (0-100%) + return percentage <= 100; + } + + /// + /// Validates that a raw VCP value is within the monitor's supported range + /// + /// Raw VCP value to validate + /// Maximum value supported by the monitor for this VCP code + /// True if the value is within the valid range (0 to maxValue) + public static bool IsValidRawVcpValue(uint value, uint maxValue) + { + return value <= maxValue; + } + + /// + /// Validates that a percentage value is in the valid range + /// + /// Percentage value to validate + /// True if the percentage is between 0 and 100 inclusive + public static bool IsValidPercentage(uint percentage) + { + return percentage <= 100; + } + + /// + /// Validates that a VCP code is in the valid range + /// + /// VCP code to validate + /// True if the VCP code is in the valid range (0x00-0xFF) + public static bool IsValidVcpCode(byte vcpCode) + { + // All byte values are valid VCP codes (0x00-0xFF) + return true; + } + + /// + /// Gets a descriptive error message for invalid percentage values + /// + /// The invalid input that was provided + /// Error message describing the validation failure + public static string GetPercentageValidationError(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return "Percentage value cannot be empty"; + } + + var cleanInput = input.Trim(); + if (cleanInput.EndsWith("%")) + { + cleanInput = cleanInput.Substring(0, cleanInput.Length - 1).Trim(); + } + + if (!uint.TryParse(cleanInput, out uint value)) + { + return $"'{input}' is not a valid percentage value. Expected format: 0-100 or 0%-100%"; + } + + if (value > 100) + { + return $"Percentage value {value}% is out of range. Valid range: 0-100%"; + } + + return $"'{input}' is not a valid percentage value"; + } + + /// + /// Gets a descriptive error message for invalid VCP codes + /// + /// The invalid input that was provided + /// Error message describing the validation failure + public static string GetVcpCodeValidationError(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return "VCP code cannot be empty"; + } + + var cleanInput = input.Trim(); + + if (cleanInput.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return $"'{input}' is not a valid VCP code. Expected hex format: 0x00-0xFF"; + } + + if (uint.TryParse(cleanInput, out uint value) && value > 255) + { + return $"VCP code {value} is out of range. Valid range: 0-255 (0x00-0xFF)"; + } + + return $"'{input}' is not a valid VCP code. Expected format: 0-255 or 0x00-0xFF"; + } + + /// + /// Gets a descriptive error message for invalid raw VCP values + /// + /// The invalid value that was provided + /// The maximum value supported by the monitor + /// The name of the VCP feature + /// Error message describing the validation failure + public static string GetRawValueValidationError(uint value, uint maxValue, string featureName) + { + return $"Value {value} is out of range for {featureName}. Valid range: 0-{maxValue}"; + } + + /// + /// Gets all known feature names including aliases + /// + /// Collection of known feature names + public static IEnumerable GetKnownFeatureNames() + { + return FeatureMap.Keys; + } + + /// + /// Gets all predefined VCP features + /// + /// Collection of predefined VCP features + public static IEnumerable GetPredefinedFeatures() + { + return VcpFeature.AllFeatures; + } +} \ No newline at end of file diff --git a/DDCSwitch/JsonContext.cs b/DDCSwitch/JsonContext.cs index f33dcde..059cdf0 100644 --- a/DDCSwitch/JsonContext.cs +++ b/DDCSwitch/JsonContext.cs @@ -5,8 +5,11 @@ namespace DDCSwitch; [JsonSerializable(typeof(ErrorResponse))] [JsonSerializable(typeof(ListMonitorsResponse))] [JsonSerializable(typeof(MonitorInfo))] -[JsonSerializable(typeof(GetInputResponse))] -[JsonSerializable(typeof(SetInputResponse))] +[JsonSerializable(typeof(GetVcpResponse))] +[JsonSerializable(typeof(SetVcpResponse))] +[JsonSerializable(typeof(VcpScanResponse))] +[JsonSerializable(typeof(VcpFeatureInfo))] +[JsonSerializable(typeof(VcpFeatureType))] [JsonSerializable(typeof(MonitorReference))] [JsonSourceGenerationOptions( WriteIndented = true, @@ -20,8 +23,9 @@ internal partial class JsonContext : JsonSerializerContext // Response models internal record ErrorResponse(bool Success, string Error, MonitorReference? Monitor = null); internal record ListMonitorsResponse(bool Success, List? Monitors = null, string? Error = null); -internal record GetInputResponse(bool Success, MonitorReference Monitor, string CurrentInput, string CurrentInputCode, uint MaxValue); -internal record SetInputResponse(bool Success, MonitorReference Monitor, string NewInput, string NewInputCode); +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 VcpScanResponse(bool Success, MonitorReference Monitor, List Features, string? ErrorMessage = null); // Data models internal record MonitorInfo( @@ -31,7 +35,9 @@ internal record MonitorInfo( bool IsPrimary, string? CurrentInput, string? CurrentInputCode, - string Status); + string Status, + string? Brightness = null, + string? Contrast = null); internal record MonitorReference(int Index, string Name, string DeviceName, bool IsPrimary = false); diff --git a/DDCSwitch/Monitor.cs b/DDCSwitch/Monitor.cs index ffcba81..00dab9a 100644 --- a/DDCSwitch/Monitor.cs +++ b/DDCSwitch/Monitor.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + namespace DDCSwitch; /// @@ -42,6 +47,164 @@ public bool TrySetInputSource(uint value) return NativeMethods.SetVCPFeature(Handle, InputSource.VcpInputSource, value); } + /// + /// Attempts to read a VCP feature value from the monitor with enhanced error detection + /// + /// VCP code to read (0x00-0xFF) + /// Current value of the VCP feature + /// Maximum value supported by the VCP feature + /// Win32 error code if operation fails + /// True if the operation was successful + public bool TryGetVcpFeature(byte vcpCode, out uint currentValue, out uint maxValue, out int errorCode) + { + currentValue = 0; + maxValue = 0; + errorCode = 0; + + if (_disposed || Handle == IntPtr.Zero) + { + errorCode = 0x00000006; // ERROR_INVALID_HANDLE + return false; + } + + bool success = NativeMethods.GetVCPFeatureAndVCPFeatureReply( + Handle, + vcpCode, + out _, + out currentValue, + out maxValue); + + if (!success) + { + errorCode = Marshal.GetLastWin32Error(); + } + + return success; + } + + /// + /// Attempts to read a VCP feature value from the monitor (legacy method for backward compatibility) + /// + /// VCP code to read (0x00-0xFF) + /// Current value of the VCP feature + /// Maximum value supported by the VCP feature + /// True if the operation was successful + public bool TryGetVcpFeature(byte vcpCode, out uint currentValue, out uint maxValue) + { + return TryGetVcpFeature(vcpCode, out currentValue, out maxValue, out _); + } + + /// + /// Attempts to write a VCP feature value to the monitor with enhanced error detection + /// + /// VCP code to write (0x00-0xFF) + /// Value to set for the VCP feature + /// Win32 error code if operation fails + /// True if the operation was successful + public bool TrySetVcpFeature(byte vcpCode, uint value, out int errorCode) + { + errorCode = 0; + + if (_disposed || Handle == IntPtr.Zero) + { + errorCode = 0x00000006; // ERROR_INVALID_HANDLE + return false; + } + + bool success = NativeMethods.SetVCPFeature(Handle, vcpCode, value); + + if (!success) + { + errorCode = Marshal.GetLastWin32Error(); + } + + return success; + } + + /// + /// Attempts to write a VCP feature value to the monitor (legacy method for backward compatibility) + /// + /// VCP code to write (0x00-0xFF) + /// Value to set for the VCP feature + /// True if the operation was successful + public bool TrySetVcpFeature(byte vcpCode, uint value) + { + return TrySetVcpFeature(vcpCode, value, out _); + } + + /// + /// Scans all VCP codes (0x00-0xFF) to discover supported features + /// + /// Dictionary mapping VCP codes to their feature information + public Dictionary ScanVcpFeatures() + { + var features = new Dictionary(); + + if (_disposed || Handle == IntPtr.Zero) + { + return features; + } + + // Get predefined features for name lookup + var predefinedFeatures = FeatureResolver.GetPredefinedFeatures() + .ToDictionary(f => f.Code, f => f); + + // Scan all possible VCP codes (0x00 to 0xFF) + for (int code = 0; code <= 255; code++) + { + byte vcpCode = (byte)code; + + if (TryGetVcpFeature(vcpCode, out uint currentValue, out uint maxValue)) + { + // Feature is supported - determine name and type + string name; + VcpFeatureType type; + + if (predefinedFeatures.TryGetValue(vcpCode, out VcpFeature? predefined)) + { + name = predefined.Name; + type = predefined.Type; + } + else + { + name = $"VCP_{vcpCode:X2}"; + type = VcpFeatureType.ReadWrite; // Assume read-write for unknown codes + } + + features[vcpCode] = new VcpFeatureInfo( + vcpCode, + name, + predefined?.Description ?? $"VCP feature {name}", + type, + predefined?.Category ?? VcpFeatureCategory.Miscellaneous, + currentValue, + maxValue, + true + ); + } + else + { + // Feature is not supported - still add entry for completeness + string name = predefinedFeatures.TryGetValue(vcpCode, out VcpFeature? predefined) + ? predefined.Name + : $"VCP_{vcpCode:X2}"; + + features[vcpCode] = new VcpFeatureInfo( + vcpCode, + name, + predefined?.Description ?? $"VCP feature {name}", + VcpFeatureType.ReadWrite, + predefined?.Category ?? VcpFeatureCategory.Miscellaneous, + 0, + 0, + false + ); + } + } + + return features; + } + public void Dispose() { if (!_disposed && Handle != IntPtr.Zero) diff --git a/DDCSwitch/Program.cs b/DDCSwitch/Program.cs index 925e737..0cac76e 100644 --- a/DDCSwitch/Program.cs +++ b/DDCSwitch/Program.cs @@ -1,528 +1,4 @@ -using DDCSwitch; -using Spectre.Console; -using System.Text.Json; -using System.Text.Json.Serialization; +using DDCSwitch.Commands; -return DDCSwitchProgram.Run(args); +return CommandRouter.Route(args); -static class DDCSwitchProgram -{ - private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - TypeInfoResolver = JsonContext.Default - }; - - public static int Run(string[] args) - { - if (args.Length == 0) - { - ShowUsage(); - return 1; - } - - // Check for --json flag - bool jsonOutput = args.Contains("--json", StringComparer.OrdinalIgnoreCase); - var filteredArgs = args.Where(a => !a.Equals("--json", StringComparison.OrdinalIgnoreCase)).ToArray(); - - if (filteredArgs.Length == 0) - { - ShowUsage(); - return 1; - } - - var command = filteredArgs[0].ToLowerInvariant(); - - try - { - return command switch - { - "list" or "ls" => ListMonitors(jsonOutput), - "get" => GetCurrentInput(filteredArgs, jsonOutput), - "set" => SetInput(filteredArgs, jsonOutput), - "version" or "-v" or "--version" => ShowVersion(jsonOutput), - "help" or "-h" or "--help" or "/?" => ShowUsage(), - _ => InvalidCommand(filteredArgs[0], jsonOutput) - }; - } - catch (Exception ex) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, ex.Message); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}"); - } - - return 1; - } - } - - private static string GetVersion() - { - var version = typeof(DDCSwitchProgram).Assembly - .GetName().Version?.ToString(3) ?? "0.0.0"; - return version; - } - - private static int ShowVersion(bool jsonOutput) - { - var version = GetVersion(); - - if (jsonOutput) - { - Console.WriteLine($"{{\"version\":\"{version}\"}}"); - } - else - { - AnsiConsole.Write(new FigletText("DDCSwitch").Color(Color.Blue)); - AnsiConsole.MarkupLine($"[bold]Version:[/] [green]{version}[/]"); - AnsiConsole.MarkupLine("[dim]Windows DDC/CI Monitor Input Switcher[/]"); - } - - return 0; - } - - private static int ShowUsage() - { - var version = GetVersion(); - - AnsiConsole.Write(new FigletText("DDCSwitch").Color(Color.Blue)); - AnsiConsole.MarkupLine($"[dim]Windows DDC/CI Monitor Input Switcher v{version}[/]\n"); - - var table = new Table() - .Border(TableBorder.Rounded) - .AddColumn("Command") - .AddColumn("Description") - .AddColumn("Example"); - - table.AddRow( - "[yellow]list[/] or [yellow]ls[/]", - "List all DDC/CI capable monitors", - "[dim]DDCSwitch list[/]"); - - table.AddRow( - "[yellow]get[/] ", - "Get current input source for a monitor", - "[dim]DDCSwitch get 0[/]"); - - table.AddRow( - "[yellow]set[/] ", - "Set input source for a monitor", - "[dim]DDCSwitch set 0 HDMI1[/]"); - - table.AddRow( - "[yellow]version[/] or [yellow]-v[/]", - "Display version information", - "[dim]DDCSwitch version[/]"); - - AnsiConsole.Write(table); - - AnsiConsole.MarkupLine("\n[bold]Monitor:[/] Monitor index (0, 1, 2...) or name pattern"); - AnsiConsole.MarkupLine("[bold]Input:[/] Input name (HDMI1, HDMI2, DP1, DP2, DVI1, VGA1) or hex code (0x11)"); - AnsiConsole.MarkupLine("\n[bold]Options:[/]"); - AnsiConsole.MarkupLine(" [yellow]--json[/] Output in JSON format"); - - return 0; - } - - private static int InvalidCommand(string command, bool jsonOutput) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, $"Unknown command: {command}"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Unknown command:[/] {command}"); - AnsiConsole.MarkupLine("Run [yellow]DDCSwitch help[/] for usage information."); - } - - return 1; - } - - private static int ListMonitors(bool jsonOutput) - { - if (!jsonOutput) - { - AnsiConsole.Status() - .Start("Enumerating monitors...", ctx => - { - ctx.Spinner(Spinner.Known.Dots); - Thread.Sleep(100); // Brief pause for visual feedback - }); - } - - var monitors = MonitorController.EnumerateMonitors(); - - if (monitors.Count == 0) - { - if (jsonOutput) - { - var result = new ListMonitorsResponse(false, null, "No DDC/CI capable monitors found"); - Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.ListMonitorsResponse)); - } - else - { - AnsiConsole.MarkupLine("[yellow]No DDC/CI capable monitors found.[/]"); - } - - return 1; - } - - if (jsonOutput) - { - var monitorList = monitors.Select(monitor => - { - string? inputName = null; - uint? inputCode = null; - string status = "ok"; - - try - { - if (monitor.TryGetInputSource(out uint current, out uint max)) - { - inputName = InputSource.GetName(current); - inputCode = current; - } - else - { - status = "no_ddc_ci"; - } - } - catch - { - status = "error"; - } - - return new MonitorInfo( - monitor.Index, - monitor.Name, - monitor.DeviceName, - monitor.IsPrimary, - inputName, - inputCode != null ? $"0x{inputCode:X2}" : null, - status); - }).ToList(); - - var result = new ListMonitorsResponse(true, monitorList); - Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.ListMonitorsResponse)); - } - else - { - var table = new Table() - .Border(TableBorder.Rounded) - .AddColumn("Index") - .AddColumn("Monitor Name") - .AddColumn("Device") - .AddColumn("Current Input") - .AddColumn("Status"); - - foreach (var monitor in monitors) - { - string inputInfo = "N/A"; - string status = "[green]OK[/]"; - - try - { - if (monitor.TryGetInputSource(out uint current, out uint max)) - { - inputInfo = $"{InputSource.GetName(current)} (0x{current:X2})"; - } - else - { - status = "[yellow]No DDC/CI[/]"; - } - } - catch - { - status = "[red]Error[/]"; - } - - table.AddRow( - monitor.IsPrimary ? $"{monitor.Index} [yellow]*[/]" : monitor.Index.ToString(), - monitor.Name, - monitor.DeviceName, - inputInfo, - status); - } - - AnsiConsole.Write(table); - } - - // Cleanup - foreach (var monitor in monitors) - { - monitor.Dispose(); - } - - return 0; - } - - private static int GetCurrentInput(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 - { - AnsiConsole.MarkupLine("[red]Error:[/] Monitor identifier required."); - AnsiConsole.MarkupLine("Usage: [yellow]DDCSwitch get [/]"); - } - - return 1; - } - - var monitors = MonitorController.EnumerateMonitors(); - - if (monitors.Count == 0) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, "No DDC/CI capable monitors found"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine("[red]Error:[/] No DDC/CI capable monitors found."); - } - - return 1; - } - - var monitor = MonitorController.FindMonitor(monitors, args[1]); - - if (monitor == null) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, $"Monitor '{args[1]}' not found"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Error:[/] Monitor '{args[1]}' not found."); - AnsiConsole.MarkupLine("Use [yellow]DDCSwitch list[/] to see available monitors."); - } - - // Cleanup - foreach (var m in monitors) - { - m.Dispose(); - } - - return 1; - } - - if (!monitor.TryGetInputSource(out uint current, out uint max)) - { - if (jsonOutput) - { - var monitorRef = - new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); - var error = new ErrorResponse(false, $"Failed to get input source from monitor '{monitor.Name}'", - monitorRef); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Error:[/] Failed to get input source from monitor '{monitor.Name}'."); - AnsiConsole.MarkupLine("The monitor may not support DDC/CI or requires administrator privileges."); - } - - // Cleanup - foreach (var m in monitors) - { - m.Dispose(); - } - - return 1; - } - - if (jsonOutput) - { - var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); - var result = new GetInputResponse(true, monitorRef, InputSource.GetName(current), $"0x{current:X2}", max); - Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.GetInputResponse)); - } - else - { - AnsiConsole.MarkupLine($"[green]Monitor:[/] {monitor.Name} ({monitor.DeviceName})"); - AnsiConsole.MarkupLine($"[green]Current Input:[/] {InputSource.GetName(current)} (0x{current:X2})"); - } - - // Cleanup - foreach (var m in monitors) - { - m.Dispose(); - } - - return 0; - } - - private static int SetInput(string[] args, bool jsonOutput) - { - if (args.Length < 3) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, "Monitor and input source required"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine("[red]Error:[/] Monitor and input source required."); - AnsiConsole.MarkupLine("Usage: [yellow]DDCSwitch set [/]"); - } - - return 1; - } - - if (!InputSource.TryParse(args[2], out uint inputValue)) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, $"Invalid input source '{args[2]}'"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Error:[/] Invalid input source '{args[2]}'."); - AnsiConsole.MarkupLine( - "Valid inputs: HDMI1, HDMI2, DP1, DP2, DVI1, DVI2, VGA1, VGA2, or hex code (0x11)"); - } - - return 1; - } - - var monitors = MonitorController.EnumerateMonitors(); - - if (monitors.Count == 0) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, "No DDC/CI capable monitors found"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine("[red]Error:[/] No DDC/CI capable monitors found."); - } - - return 1; - } - - var monitor = MonitorController.FindMonitor(monitors, args[1]); - - if (monitor == null) - { - if (jsonOutput) - { - var error = new ErrorResponse(false, $"Monitor '{args[1]}' not found"); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Error:[/] Monitor '{args[1]}' not found."); - AnsiConsole.MarkupLine("Use [yellow]DDCSwitch list[/] to see available monitors."); - } - - // Cleanup - foreach (var m in monitors) - { - m.Dispose(); - } - - return 1; - } - - bool success = false; - string? errorMsg = null; - - if (!jsonOutput) - { - AnsiConsole.Status() - .Start($"Switching {monitor.Name} to {InputSource.GetName(inputValue)}...", ctx => - { - ctx.Spinner(Spinner.Known.Dots); - - if (!monitor.TrySetInputSource(inputValue)) - { - errorMsg = - $"Failed to set input source on monitor '{monitor.Name}'. The monitor may not support this input or requires administrator privileges."; - } - else - { - success = true; - // Give the monitor a moment to switch - Thread.Sleep(500); - } - }); - } - else - { - if (!monitor.TrySetInputSource(inputValue)) - { - errorMsg = $"Failed to set input source on monitor '{monitor.Name}'"; - } - else - { - success = true; - Thread.Sleep(500); - } - } - - if (!success) - { - if (jsonOutput) - { - var monitorRef = - new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); - var error = new ErrorResponse(false, errorMsg!, monitorRef); - Console.WriteLine(JsonSerializer.Serialize(error, JsonContext.Default.ErrorResponse)); - } - else - { - AnsiConsole.MarkupLine($"[red]Error:[/] {errorMsg}"); - } - - // Cleanup - foreach (var m in monitors) - { - m.Dispose(); - } - - return 1; - } - - if (jsonOutput) - { - var monitorRef = new MonitorReference(monitor.Index, monitor.Name, monitor.DeviceName, monitor.IsPrimary); - var result = new SetInputResponse(true, monitorRef, InputSource.GetName(inputValue), $"0x{inputValue:X2}"); - Console.WriteLine(JsonSerializer.Serialize(result, JsonContext.Default.SetInputResponse)); - } - else - { - AnsiConsole.MarkupLine( - $"[green]✓[/] Successfully switched {monitor.Name} to {InputSource.GetName(inputValue)}"); - } - - // Cleanup - foreach (var m in monitors) - { - m.Dispose(); - } - - return 0; - } -} \ No newline at end of file diff --git a/DDCSwitch/Properties/launchSettings.json b/DDCSwitch/Properties/launchSettings.json new file mode 100644 index 0000000..6e77e58 --- /dev/null +++ b/DDCSwitch/Properties/launchSettings.json @@ -0,0 +1,33 @@ +{ + "profiles": { + "ddcswitch (get 0)": { + "commandName": "Project", + "commandLineArgs": "get 0" + }, + "ddcswitch (list)": { + "commandName": "Project", + "commandLineArgs": "list" + }, + "ddcswitch (list --verbose)": { + "commandName": "Project", + "commandLineArgs": "list --verbose" + }, + "ddcswitch (set)": { + "commandName": "Project", + "commandLineArgs": "set 0 input HDMI1" + }, + "ddcswitch (version)": { + "commandName": "Project", + "commandLineArgs": "version" + }, + "ddcswitch (help)": { + "commandName": "Project", + "commandLineArgs": "help" + }, + "ddcswitch (error command)": { + "commandName": "Project", + "commandLineArgs": "gettt" + } + } +} + diff --git a/DDCSwitch/VcpErrorHandler.cs b/DDCSwitch/VcpErrorHandler.cs new file mode 100644 index 0000000..c210331 --- /dev/null +++ b/DDCSwitch/VcpErrorHandler.cs @@ -0,0 +1,259 @@ +using System.Runtime.InteropServices; + +namespace DDCSwitch; + +/// +/// Provides enhanced error handling for VCP operations with specific error messages and suggestions +/// +public static class VcpErrorHandler +{ + /// + /// Creates a detailed error message for VCP read failures + /// + /// The monitor that failed + /// The VCP feature that failed + /// Detailed error message with suggestions + public static string CreateReadFailureMessage(Monitor monitor, VcpFeature feature) + { + var baseMessage = $"Failed to read {feature.Name} from monitor '{monitor.Name}'"; + var suggestions = GetReadFailureSuggestions(feature); + + return $"{baseMessage}. {suggestions}"; + } + + /// + /// Creates a detailed error message for VCP write failures + /// + /// The monitor that failed + /// The VCP feature that failed + /// The value that was attempted to be set + /// Detailed error message with suggestions + public static string CreateWriteFailureMessage(Monitor monitor, VcpFeature feature, uint attemptedValue) + { + var baseMessage = $"Failed to set {feature.Name} to {attemptedValue} on monitor '{monitor.Name}'"; + var suggestions = GetWriteFailureSuggestions(feature, attemptedValue); + + return $"{baseMessage}. {suggestions}"; + } + + /// + /// Creates a detailed error message for unsupported VCP features + /// + /// The monitor that doesn't support the feature + /// The unsupported VCP feature + /// Detailed error message with alternatives + public static string CreateUnsupportedFeatureMessage(Monitor monitor, VcpFeature feature) + { + var baseMessage = $"Monitor '{monitor.Name}' doesn't support {feature.Name} control (VCP 0x{feature.Code:X2})"; + var alternatives = GetFeatureAlternatives(feature); + + return $"{baseMessage}. {alternatives}"; + } + + /// + /// Creates a detailed error message for VCP communication timeouts + /// + /// The monitor that timed out + /// The VCP feature that timed out + /// The operation that timed out (read/write) + /// Detailed error message with troubleshooting steps + public static string CreateTimeoutMessage(Monitor monitor, VcpFeature feature, string operation) + { + var baseMessage = $"Timeout while trying to {operation} {feature.Name} on monitor '{monitor.Name}'"; + var troubleshooting = GetTimeoutTroubleshooting(); + + return $"{baseMessage}. {troubleshooting}"; + } + + /// + /// Creates a detailed error message for DDC/CI communication failures + /// + /// The monitor with communication issues + /// Detailed error message with troubleshooting steps + public static string CreateCommunicationFailureMessage(Monitor monitor) + { + var baseMessage = $"DDC/CI communication failed with monitor '{monitor.Name}'"; + var troubleshooting = GetCommunicationTroubleshooting(); + + return $"{baseMessage}. {troubleshooting}"; + } + + /// + /// Creates a detailed error message for value range validation failures + /// + /// The VCP feature + /// The invalid value + /// The maximum allowed value + /// Whether the value is a percentage + /// Detailed error message with valid range information + public static string CreateRangeValidationMessage(VcpFeature feature, uint attemptedValue, uint maxValue, bool isPercentage = false) + { + if (isPercentage) + { + return $"{feature.Name} value {attemptedValue}% is out of range. Valid range: 0-100%"; + } + + var baseMessage = $"{feature.Name} value {attemptedValue} is out of range for this monitor"; + var validRange = $"Valid range: 0-{maxValue}"; + var suggestion = GetRangeValidationSuggestion(feature, maxValue); + + return $"{baseMessage}. {validRange}. {suggestion}"; + } + + /// + /// Determines if a VCP operation failure is likely due to a timeout + /// + /// The last Win32 error code + /// True if the error indicates a timeout + public static bool IsTimeoutError(int lastError) + { + // Common timeout-related error codes + return lastError switch + { + 0x00000102 => true, // ERROR_TIMEOUT + 0x00000121 => true, // ERROR_SEM_TIMEOUT + 0x000005B4 => true, // ERROR_TIMEOUT (alternative) + 0x00000079 => true, // ERROR_SEM_TIMEOUT (alternative) + _ => false + }; + } + + /// + /// Determines if a VCP operation failure is likely due to unsupported feature + /// + /// The last Win32 error code + /// True if the error indicates unsupported feature + public static bool IsUnsupportedFeatureError(int lastError) + { + // Common unsupported feature error codes + return lastError switch + { + 0x00000001 => true, // ERROR_INVALID_FUNCTION + 0x00000057 => true, // ERROR_INVALID_PARAMETER + 0x0000007A => true, // ERROR_INSUFFICIENT_BUFFER + 0x00000032 => true, // ERROR_NOT_SUPPORTED + _ => false + }; + } + + /// + /// Gets suggestions for VCP read failures + /// + private static string GetReadFailureSuggestions(VcpFeature feature) + { + var suggestions = new List(); + + if (feature.Code == VcpFeature.Brightness.Code || feature.Code == VcpFeature.Contrast.Code) + { + suggestions.Add("Some monitors require administrator privileges for brightness/contrast control"); + suggestions.Add("Try running as administrator or check if the monitor supports DDC/CI for this feature"); + } + else if (feature.Code == VcpFeature.InputSource.Code) + { + suggestions.Add("Ensure the monitor supports DDC/CI input switching"); + suggestions.Add("Some monitors only support input switching when not in use"); + } + else + { + suggestions.Add($"VCP code 0x{feature.Code:X2} may not be supported by this monitor"); + suggestions.Add("Use 'ddcswitch get ' to see all supported VCP codes"); + } + + suggestions.Add("Check that the monitor is properly connected and powered on"); + + return string.Join(". ", suggestions); + } + + /// + /// Gets suggestions for VCP write failures + /// + private static string GetWriteFailureSuggestions(VcpFeature feature, uint attemptedValue) + { + var suggestions = new List(); + + if (feature.Code == VcpFeature.Brightness.Code || feature.Code == VcpFeature.Contrast.Code) + { + suggestions.Add("Some monitors require administrator privileges for brightness/contrast control"); + suggestions.Add("Ensure the value is within the monitor's supported range (use 'get' command to check current range)"); + } + else if (feature.Code == VcpFeature.InputSource.Code) + { + suggestions.Add("Verify the input source is available on this monitor"); + suggestions.Add("Some monitors only allow input switching when the input is not active"); + } + else + { + suggestions.Add($"VCP code 0x{feature.Code:X2} may not support write operations on this monitor"); + suggestions.Add("Use 'ddcswitch get ' to see all supported VCP codes"); + } + + suggestions.Add("Try running as administrator if permission issues persist"); + + return string.Join(". ", suggestions); + } + + /// + /// Gets alternative features when a feature is unsupported + /// + private static string GetFeatureAlternatives(VcpFeature feature) + { + return feature.Code switch + { + 0x10 => "Try using your monitor's physical buttons or on-screen display (OSD) to adjust brightness", + 0x12 => "Try using your monitor's physical buttons or on-screen display (OSD) to adjust contrast", + 0x60 => "Try using your monitor's physical input selection button or check if the monitor supports other input switching methods", + _ => "Use 'ddcswitch get ' to see all supported VCP codes" + }; + } + + /// + /// Gets troubleshooting steps for timeout errors + /// + private static string GetTimeoutTroubleshooting() + { + var steps = new List + { + "The monitor may be busy or slow to respond", + "Try waiting a moment and running the command again", + "Check that no other DDC/CI applications are accessing the monitor", + "Ensure the monitor cable supports DDC/CI communication (some cheap cables don't)", + "Try power cycling the monitor if the issue persists" + }; + + return string.Join(". ", steps); + } + + /// + /// Gets troubleshooting steps for general communication failures + /// + private static string GetCommunicationTroubleshooting() + { + var steps = new List + { + "Ensure the monitor supports DDC/CI (check monitor documentation)", + "Verify the video cable supports DDC/CI (HDMI, DisplayPort, DVI-D, or VGA with DDC support)", + "Try running as administrator - some monitors require elevated privileges", + "Check if DDC/CI is enabled in the monitor's on-screen display (OSD) settings", + "Power cycle the monitor and try again" + }; + + return string.Join(". ", steps); + } + + /// + /// Gets suggestions for range validation failures + /// + private static string GetRangeValidationSuggestion(VcpFeature feature, uint maxValue) + { + if (feature.SupportsPercentage) + { + return "For percentage values, use 0-100% format (e.g., '75%')"; + } + + return feature.Code switch + { + 0x60 => "For input sources, use names like 'HDMI1', 'DP1', or hex codes like '0x11'", + _ => $"Use 'ddcswitch get {feature.Name}' to see the current value and valid range" + }; + } +} \ No newline at end of file diff --git a/DDCSwitch/VcpFeature.Generated.cs b/DDCSwitch/VcpFeature.Generated.cs new file mode 100644 index 0000000..554118c --- /dev/null +++ b/DDCSwitch/VcpFeature.Generated.cs @@ -0,0 +1,935 @@ +// +// This file is automatically generated from VcpFeatureData.json +// Do not edit this file directly. Instead, edit VcpFeatureData.json and regenerate. + +namespace DDCSwitch; + +public partial class VcpFeature +{ + // ===== GENERATED VCP FEATURES ===== + + // ===== PRESET FEATURES ===== + + /// + /// Degauss (VCP 0x01) + /// + public static VcpFeature Degauss => new(0x01, "degauss", "Degauss", VcpFeatureType.WriteOnly, VcpFeatureCategory.Preset, false); + + /// + /// New control value (VCP 0x02) + /// + public static VcpFeature NewControlValue => new(0x02, "new-control-value", "New control value", VcpFeatureType.ReadWrite, VcpFeatureCategory.Preset, false); + + /// + /// Soft controls (VCP 0x03) + /// + public static VcpFeature SoftControls => new(0x03, "soft-controls", "Soft controls", VcpFeatureType.ReadWrite, VcpFeatureCategory.Preset, false); + + /// + /// Restore factory defaults (VCP 0x04) + /// + public static VcpFeature RestoreDefaults => new(0x04, "restore-defaults", "Restore factory defaults", VcpFeatureType.WriteOnly, VcpFeatureCategory.Preset, false, "factory-reset"); + + /// + /// Restore factory brightness/contrast defaults (VCP 0x05) + /// + public static VcpFeature RestoreBrightnessContrastDefaults => new(0x05, "restore-brightness-contrast", "Restore factory brightness/contrast defaults", VcpFeatureType.WriteOnly, VcpFeatureCategory.Preset, false); + + /// + /// Restore factory geometry defaults (VCP 0x06) + /// + public static VcpFeature RestoreGeometryDefaults => new(0x06, "restore-geometry", "Restore factory geometry defaults", VcpFeatureType.WriteOnly, VcpFeatureCategory.Preset, false); + + /// + /// Restore factory color defaults (VCP 0x08) + /// + public static VcpFeature RestoreColorDefaults => new(0x08, "restore-color", "Restore factory color defaults", VcpFeatureType.WriteOnly, VcpFeatureCategory.Preset, false); + + /// + /// Restore factory TV defaults (VCP 0x0A) + /// + public static VcpFeature RestoreTVDefaults => new(0x0A, "restore-tv-defaults", "Restore factory TV defaults", VcpFeatureType.WriteOnly, VcpFeatureCategory.Preset, false); + + + // ===== COLORCONTROL FEATURES ===== + + /// + /// Color temperature increment (VCP 0x0B) + /// + public static VcpFeature ColorTemperatureIncrement => new(0x0B, "color-temp-increment", "Color temperature increment", VcpFeatureType.ReadOnly, VcpFeatureCategory.ColorControl, false); + + /// + /// Color temperature request (VCP 0x0C) + /// + public static VcpFeature ColorTemperatureRequest => new(0x0C, "color-temp-request", "Color temperature request", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, false, "color-temp"); + + /// + /// Video gain: Red (VCP 0x16) + /// + public static VcpFeature RedGain => new(0x16, "red-gain", "Video gain: Red", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true, "red"); + + /// + /// User color vision compensation (VCP 0x17) + /// + public static VcpFeature UserColorVisionCompensation => new(0x17, "user-color-vision-compensation", "User color vision compensation", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Video gain: Green (VCP 0x18) + /// + public static VcpFeature GreenGain => new(0x18, "green-gain", "Video gain: Green", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true, "green"); + + /// + /// Video gain: Blue (VCP 0x1A) + /// + public static VcpFeature BlueGain => new(0x1A, "blue-gain", "Video gain: Blue", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true, "blue"); + + /// + /// 6 axis saturation: Red (VCP 0x59) + /// + public static VcpFeature SixAxisSaturationRed => new(0x59, "6-axis-saturation-red", "6 axis saturation: Red", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis saturation: Yellow (VCP 0x5A) + /// + public static VcpFeature SixAxisSaturationYellow => new(0x5A, "6-axis-saturation-yellow", "6 axis saturation: Yellow", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis saturation: Green (VCP 0x5B) + /// + public static VcpFeature SixAxisSaturationGreen => new(0x5B, "6-axis-saturation-green", "6 axis saturation: Green", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis saturation: Cyan (VCP 0x5C) + /// + public static VcpFeature SixAxisSaturationCyan => new(0x5C, "6-axis-saturation-cyan", "6 axis saturation: Cyan", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis saturation: Blue (VCP 0x5D) + /// + public static VcpFeature SixAxisSaturationBlue => new(0x5D, "6-axis-saturation-blue", "6 axis saturation: Blue", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis saturation: Magenta (VCP 0x5E) + /// + public static VcpFeature SixAxisSaturationMagenta => new(0x5E, "6-axis-saturation-magenta", "6 axis saturation: Magenta", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Backlight Level: White (VCP 0x6B) + /// + public static VcpFeature BacklightLevelWhite => new(0x6B, "backlight-level-white", "Backlight Level: White", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Video black level: Red (VCP 0x6C) + /// + public static VcpFeature RedBlackLevel => new(0x6C, "red-black-level", "Video black level: Red", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Backlight Level: Red (VCP 0x6D) + /// + public static VcpFeature BacklightLevelRed => new(0x6D, "backlight-level-red", "Backlight Level: Red", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Video black level: Green (VCP 0x6E) + /// + public static VcpFeature GreenBlackLevel => new(0x6E, "green-black-level", "Video black level: Green", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Backlight Level: Green (VCP 0x6F) + /// + public static VcpFeature BacklightLevelGreen => new(0x6F, "backlight-level-green", "Backlight Level: Green", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Video black level: Blue (VCP 0x70) + /// + public static VcpFeature BlueBlackLevel => new(0x70, "blue-black-level", "Video black level: Blue", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Backlight Level: Blue (VCP 0x71) + /// + public static VcpFeature BacklightLevelBlue => new(0x71, "backlight-level-blue", "Backlight Level: Blue", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// Gamma (VCP 0x72) + /// + public static VcpFeature Gamma => new(0x72, "gamma", "Gamma", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, false); + + /// + /// Color Saturation (VCP 0x8A) + /// + public static VcpFeature ColorSaturation => new(0x8A, "color-saturation", "Color Saturation", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true, "saturation", "sat"); + + /// + /// Hue (VCP 0x90) + /// + public static VcpFeature Hue => new(0x90, "hue", "Hue", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis hue control: Red (VCP 0x9B) + /// + public static VcpFeature SixAxisHueRed => new(0x9B, "6-axis-hue-red", "6 axis hue control: Red", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis hue control: Yellow (VCP 0x9C) + /// + public static VcpFeature SixAxisHueYellow => new(0x9C, "6-axis-hue-yellow", "6 axis hue control: Yellow", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis hue control: Green (VCP 0x9D) + /// + public static VcpFeature SixAxisHueGreen => new(0x9D, "6-axis-hue-green", "6 axis hue control: Green", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis hue control: Cyan (VCP 0x9E) + /// + public static VcpFeature SixAxisHueCyan => new(0x9E, "6-axis-hue-cyan", "6 axis hue control: Cyan", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis hue control: Blue (VCP 0x9F) + /// + public static VcpFeature SixAxisHueBlue => new(0x9F, "6-axis-hue-blue", "6 axis hue control: Blue", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + /// + /// 6 axis hue control: Magenta (VCP 0xA0) + /// + public static VcpFeature SixAxisHueMagenta => new(0xA0, "6-axis-hue-magenta", "6 axis hue control: Magenta", VcpFeatureType.ReadWrite, VcpFeatureCategory.ColorControl, true); + + + // ===== GEOMETRY FEATURES ===== + + /// + /// Clock (VCP 0x0E) + /// + public static VcpFeature Clock => new(0x0E, "clock", "Clock", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Position (Phase) (VCP 0x20) + /// + public static VcpFeature HorizontalPosition => new(0x20, "h-position", "Horizontal Position (Phase)", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false, "h-pos"); + + /// + /// Horizontal Size (VCP 0x22) + /// + public static VcpFeature HorizontalSize => new(0x22, "h-size", "Horizontal Size", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Pincushion (VCP 0x24) + /// + public static VcpFeature HorizontalPincushion => new(0x24, "h-pincushion", "Horizontal Pincushion", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Pincushion Balance (VCP 0x26) + /// + public static VcpFeature HorizontalPincushionBalance => new(0x26, "h-pincushion-balance", "Horizontal Pincushion Balance", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Convergence R/B (VCP 0x28) + /// + public static VcpFeature HorizontalConvergenceRB => new(0x28, "h-convergence-rb", "Horizontal Convergence R/B", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Convergence M/G (VCP 0x29) + /// + public static VcpFeature HorizontalConvergenceMG => new(0x29, "h-convergence-mg", "Horizontal Convergence M/G", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Linearity (VCP 0x2A) + /// + public static VcpFeature HorizontalLinearity => new(0x2A, "h-linearity", "Horizontal Linearity", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Linearity Balance (VCP 0x2C) + /// + public static VcpFeature HorizontalLinearityBalance => new(0x2C, "h-linearity-balance", "Horizontal Linearity Balance", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Position (Phase) (VCP 0x30) + /// + public static VcpFeature VerticalPosition => new(0x30, "v-position", "Vertical Position (Phase)", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false, "v-pos"); + + /// + /// Vertical Size (VCP 0x32) + /// + public static VcpFeature VerticalSize => new(0x32, "v-size", "Vertical Size", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Pincushion (VCP 0x34) + /// + public static VcpFeature VerticalPincushion => new(0x34, "v-pincushion", "Vertical Pincushion", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Pincushion Balance (VCP 0x36) + /// + public static VcpFeature VerticalPincushionBalance => new(0x36, "v-pincushion-balance", "Vertical Pincushion Balance", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Convergence R/B (VCP 0x38) + /// + public static VcpFeature VerticalConvergenceRB => new(0x38, "v-convergence-rb", "Vertical Convergence R/B", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Convergence M/G (VCP 0x39) + /// + public static VcpFeature VerticalConvergenceMG => new(0x39, "v-convergence-mg", "Vertical Convergence M/G", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Linearity (VCP 0x3A) + /// + public static VcpFeature VerticalLinearity => new(0x3A, "v-linearity", "Vertical Linearity", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Linearity Balance (VCP 0x3C) + /// + public static VcpFeature VerticalLinearityBalance => new(0x3C, "v-linearity-balance", "Vertical Linearity Balance", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Clock Phase (VCP 0x3E) + /// + public static VcpFeature ClockPhase => new(0x3E, "clock-phase", "Clock Phase", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false, "phase"); + + /// + /// Horizontal Parallelogram (VCP 0x40) + /// + public static VcpFeature HorizontalParallelogram => new(0x40, "horizontal-parallelogram", "Horizontal Parallelogram", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Parallelogram (VCP 0x41) + /// + public static VcpFeature VerticalParallelogram => new(0x41, "vertical-parallelogram", "Vertical Parallelogram", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Horizontal Keystone (VCP 0x42) + /// + public static VcpFeature HorizontalKeystone => new(0x42, "horizontal-keystone", "Horizontal Keystone", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Vertical Keystone (VCP 0x43) + /// + public static VcpFeature VerticalKeystone => new(0x43, "vertical-keystone", "Vertical Keystone", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Rotation (VCP 0x44) + /// + public static VcpFeature Rotation => new(0x44, "rotation", "Rotation", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Top Corner Flare (VCP 0x46) + /// + public static VcpFeature TopCornerFlare => new(0x46, "top-corner-flare", "Top Corner Flare", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Top Corner Hook (VCP 0x48) + /// + public static VcpFeature TopCornerHook => new(0x48, "top-corner-hook", "Top Corner Hook", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Bottom Corner Flare (VCP 0x4A) + /// + public static VcpFeature BottomCornerFlare => new(0x4A, "bottom-corner-flare", "Bottom Corner Flare", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Bottom Corner Hook (VCP 0x4C) + /// + public static VcpFeature BottomCornerHook => new(0x4C, "bottom-corner-hook", "Bottom Corner Hook", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Window Position(TL_X) (VCP 0x95) + /// + public static VcpFeature WindowPositionTLX => new(0x95, "window-position-tl-x", "Window Position(TL_X)", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Window Position(TL_Y) (VCP 0x96) + /// + public static VcpFeature WindowPositionTLY => new(0x96, "window-position-tl-y", "Window Position(TL_Y)", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Window Position(BR_X) (VCP 0x97) + /// + public static VcpFeature WindowPositionBRX => new(0x97, "window-position-br-x", "Window Position(BR_X)", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Window Position(BR_Y) (VCP 0x98) + /// + public static VcpFeature WindowPositionBRY => new(0x98, "window-position-br-y", "Window Position(BR_Y)", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + /// + /// Window control on/off (VCP 0x99) + /// + public static VcpFeature WindowControlOnOff => new(0x99, "window-control-on-off", "Window control on/off", VcpFeatureType.ReadWrite, VcpFeatureCategory.Geometry, false); + + + // ===== IMAGEADJUSTMENT FEATURES ===== + + /// + /// Brightness control (VCP 0x10) + /// + public static VcpFeature Brightness => new(0x10, "brightness", "Brightness control", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true, "bright"); + + /// + /// Flesh tone enhancement (VCP 0x11) + /// + public static VcpFeature FleshToneEnhancement => new(0x11, "flesh-tone-enhancement", "Flesh tone enhancement", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Contrast control (VCP 0x12) + /// + public static VcpFeature Contrast => new(0x12, "contrast", "Contrast control", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Backlight control (VCP 0x13) + /// + public static VcpFeature Backlight => new(0x13, "backlight", "Backlight control", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Select color preset (VCP 0x14) + /// + public static VcpFeature SelectColorPreset => new(0x14, "select-color-preset", "Select color preset", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false, "color-preset"); + + /// + /// Focus (VCP 0x1C) + /// + public static VcpFeature Focus => new(0x1C, "focus", "Focus", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Auto setup (VCP 0x1E) + /// + public static VcpFeature AutoSetup => new(0x1E, "auto-setup", "Auto setup", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Auto color setup (VCP 0x1F) + /// + public static VcpFeature AutoColorSetup => new(0x1F, "auto-color-setup", "Auto color setup", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Gray scale expansion (VCP 0x2E) + /// + public static VcpFeature GrayScaleExpansion => new(0x2E, "gray-scale-expansion", "Gray scale expansion", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Horizontal Moire (VCP 0x56) + /// + public static VcpFeature HorizontalMoire => new(0x56, "horizontal-moire", "Horizontal Moire", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Vertical Moire (VCP 0x58) + /// + public static VcpFeature VerticalMoire => new(0x58, "vertical-moire", "Vertical Moire", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Input source selection (VCP 0x60) + /// + public static VcpFeature InputSource => new(0x60, "input", "Input source selection", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false, "source"); + + /// + /// Adjust Focal Plane (VCP 0x7A) + /// + public static VcpFeature AdjustFocalPlane => new(0x7A, "adjust-focal-plane", "Adjust Focal Plane", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Adjust Zoom (VCP 0x7C) + /// + public static VcpFeature AdjustZoom => new(0x7C, "adjust-zoom", "Adjust Zoom", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Trapezoid (VCP 0x7E) + /// + public static VcpFeature Trapezoid => new(0x7E, "trapezoid", "Trapezoid", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Keystone (VCP 0x80) + /// + public static VcpFeature Keystone => new(0x80, "keystone", "Keystone", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Horizontal Mirror (Flip) (VCP 0x82) + /// + public static VcpFeature HorizontalMirror => new(0x82, "horizontal-mirror", "Horizontal Mirror (Flip)", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Vertical Mirror (Flip) (VCP 0x84) + /// + public static VcpFeature VerticalMirror => new(0x84, "vertical-mirror", "Vertical Mirror (Flip)", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Display Scaling (VCP 0x86) + /// + public static VcpFeature DisplayScaling => new(0x86, "display-scaling", "Display Scaling", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Sharpness control (VCP 0x87) + /// + public static VcpFeature Sharpness => new(0x87, "sharpness", "Sharpness control", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true, "sharp"); + + /// + /// Velocity Scan Modulation (VCP 0x88) + /// + public static VcpFeature VelocityScanModulation => new(0x88, "velocity-scan-modulation", "Velocity Scan Modulation", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// TV Sharpness (VCP 0x8C) + /// + public static VcpFeature TVSharpness => new(0x8C, "tv-sharpness", "TV Sharpness", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// TV Contrast (VCP 0x8E) + /// + public static VcpFeature TVContrast => new(0x8E, "tv-contrast", "TV Contrast", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// TV Black level/Luminesence (VCP 0x92) + /// + public static VcpFeature TVBlackLevel => new(0x92, "tv-black-level", "TV Black level/Luminesence", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Window background (VCP 0x9A) + /// + public static VcpFeature WindowBackground => new(0x9A, "window-background", "Window background", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, true); + + /// + /// Auto setup on/off (VCP 0xA2) + /// + public static VcpFeature AutoSetupOnOff => new(0xA2, "auto-setup-on-off", "Auto setup on/off", VcpFeatureType.WriteOnly, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Window mask control (VCP 0xA4) + /// + public static VcpFeature WindowMaskControl => new(0xA4, "window-mask-control", "Window mask control", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Change the selected window (VCP 0xA5) + /// + public static VcpFeature ChangeSelectedWindow => new(0xA5, "change-selected-window", "Change the selected window", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Screen Orientation (VCP 0xAA) + /// + public static VcpFeature ScreenOrientation => new(0xAA, "screen-orientation", "Screen Orientation", VcpFeatureType.ReadOnly, VcpFeatureCategory.ImageAdjustment, false, "orientation"); + + /// + /// Stereo video mode (VCP 0xD4) + /// + public static VcpFeature StereoVideoMode => new(0xD4, "stereo-video-mode", "Stereo video mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Scan mode (VCP 0xDA) + /// + public static VcpFeature ScanMode => new(0xDA, "scan-mode", "Scan mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + /// + /// Image Mode (VCP 0xDB) + /// + public static VcpFeature ImageMode => new(0xDB, "image-mode", "Image Mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false, "mode"); + + /// + /// Display Mode (VCP 0xDC) + /// + public static VcpFeature DisplayMode => new(0xDC, "display-mode", "Display Mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.ImageAdjustment, false); + + + // ===== MISCELLANEOUS FEATURES ===== + + /// + /// Active control (VCP 0x52) + /// + public static VcpFeature ActiveControl => new(0x52, "active-control", "Active control", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Performance Preservation (VCP 0x54) + /// + public static VcpFeature PerformancePreservation => new(0x54, "performance-preservation", "Performance Preservation", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Ambient light sensor (VCP 0x66) + /// + public static VcpFeature AmbientLightSensor => new(0x66, "ambient-light-sensor", "Ambient light sensor", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// LUT Size (VCP 0x73) + /// + public static VcpFeature LUTSize => new(0x73, "lut-size", "LUT Size", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Single point LUT operation (VCP 0x74) + /// + public static VcpFeature SinglePointLUTOperation => new(0x74, "single-point-lut-operation", "Single point LUT operation", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Block LUT operation (VCP 0x75) + /// + public static VcpFeature BlockLUTOperation => new(0x75, "block-lut-operation", "Block LUT operation", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Remote Procedure Call (VCP 0x76) + /// + public static VcpFeature RemoteProcedureCall => new(0x76, "remote-procedure-call", "Remote Procedure Call", VcpFeatureType.WriteOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Display Identification Operation (VCP 0x78) + /// + public static VcpFeature DisplayIdentificationOperation => new(0x78, "display-identification-operation", "Display Identification Operation", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// TV Channel Up/Down (VCP 0x8B) + /// + public static VcpFeature TVChannelUpDown => new(0x8B, "tv-channel-up-down", "TV Channel Up/Down", VcpFeatureType.WriteOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Horizontal frequency (VCP 0xAC) + /// + public static VcpFeature HorizontalFrequency => new(0xAC, "horizontal-frequency", "Horizontal frequency", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false, "h-freq"); + + /// + /// Vertical frequency (VCP 0xAE) + /// + public static VcpFeature VerticalFrequency => new(0xAE, "vertical-frequency", "Vertical frequency", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false, "v-freq"); + + /// + /// Settings (VCP 0xB0) + /// + public static VcpFeature Settings => new(0xB0, "settings", "Settings", VcpFeatureType.WriteOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Flat panel sub-pixel layout (VCP 0xB2) + /// + public static VcpFeature FlatPanelSubPixelLayout => new(0xB2, "flat-panel-sub-pixel-layout", "Flat panel sub-pixel layout", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Source Timing Mode (VCP 0xB4) + /// + public static VcpFeature SourceTimingMode => new(0xB4, "source-timing-mode", "Source Timing Mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Display technology type (VCP 0xB6) + /// + public static VcpFeature DisplayTechnologyType => new(0xB6, "display-technology-type", "Display technology type", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Monitor status (VCP 0xB7) + /// + public static VcpFeature MonitorStatus => new(0xB7, "monitor-status", "Monitor status", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Packet count (VCP 0xB8) + /// + public static VcpFeature PacketCount => new(0xB8, "packet-count", "Packet count", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Monitor X origin (VCP 0xB9) + /// + public static VcpFeature MonitorXOrigin => new(0xB9, "monitor-x-origin", "Monitor X origin", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Monitor Y origin (VCP 0xBA) + /// + public static VcpFeature MonitorYOrigin => new(0xBA, "monitor-y-origin", "Monitor Y origin", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Header error count (VCP 0xBB) + /// + public static VcpFeature HeaderErrorCount => new(0xBB, "header-error-count", "Header error count", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Body CRC error count (VCP 0xBC) + /// + public static VcpFeature BodyCRCErrorCount => new(0xBC, "body-crc-error-count", "Body CRC error count", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Client ID (VCP 0xBD) + /// + public static VcpFeature ClientID => new(0xBD, "client-id", "Client ID", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Link control (VCP 0xBE) + /// + public static VcpFeature LinkControl => new(0xBE, "link-control", "Link control", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Display usage time (VCP 0xC0) + /// + public static VcpFeature DisplayUsageTime => new(0xC0, "display-usage-time", "Display usage time", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Display descriptor length (VCP 0xC2) + /// + public static VcpFeature DisplayDescriptorLength => new(0xC2, "display-descriptor-length", "Display descriptor length", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Transmit display descriptor (VCP 0xC3) + /// + public static VcpFeature TransmitDisplayDescriptor => new(0xC3, "transmit-display-descriptor", "Transmit display descriptor", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Enable display of 'display descriptor' (VCP 0xC4) + /// + public static VcpFeature EnableDisplayOfDisplayDescriptor => new(0xC4, "enable-display-of-display-descriptor", "Enable display of 'display descriptor'", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Application enable key (VCP 0xC6) + /// + public static VcpFeature ApplicationEnableKey => new(0xC6, "application-enable-key", "Application enable key", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Display controller type (VCP 0xC8) + /// + public static VcpFeature DisplayControllerType => new(0xC8, "display-controller-type", "Display controller type", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Display firmware level (VCP 0xC9) + /// + public static VcpFeature DisplayFirmwareLevel => new(0xC9, "display-firmware-level", "Display firmware level", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false, "firmware"); + + /// + /// OSD/Button Control (VCP 0xCA) + /// + public static VcpFeature OSDButtonControl => new(0xCA, "osd-button-control", "OSD/Button Control", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false, "osd"); + + /// + /// OSD Language (VCP 0xCC) + /// + public static VcpFeature OSDLanguage => new(0xCC, "osd-language", "OSD Language", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false, "language"); + + /// + /// Status Indicators (VCP 0xCD) + /// + public static VcpFeature StatusIndicators => new(0xCD, "status-indicators", "Status Indicators", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Auxiliary display size (VCP 0xCE) + /// + public static VcpFeature AuxiliaryDisplaySize => new(0xCE, "auxiliary-display-size", "Auxiliary display size", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Auxiliary display data (VCP 0xCF) + /// + public static VcpFeature AuxiliaryDisplayData => new(0xCF, "auxiliary-display-data", "Auxiliary display data", VcpFeatureType.WriteOnly, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Output select (VCP 0xD0) + /// + public static VcpFeature OutputSelect => new(0xD0, "output-select", "Output select", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Asset Tag (VCP 0xD2) + /// + public static VcpFeature AssetTag => new(0xD2, "asset-tag", "Asset Tag", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Power mode (VCP 0xD6) + /// + public static VcpFeature PowerMode => new(0xD6, "power-mode", "Power mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false, "power"); + + /// + /// Auxiliary power output (VCP 0xD7) + /// + public static VcpFeature AuxiliaryPowerOutput => new(0xD7, "auxiliary-power-output", "Auxiliary power output", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// Scratch Pad (VCP 0xDE) + /// + public static VcpFeature ScratchPad => new(0xDE, "scratch-pad", "Scratch Pad", VcpFeatureType.ReadWrite, VcpFeatureCategory.Miscellaneous, false); + + /// + /// VCP Version (VCP 0xDF) + /// + public static VcpFeature VcpVersion => new(0xDF, "vcp-version", "VCP Version", VcpFeatureType.ReadOnly, VcpFeatureCategory.Miscellaneous, false, "version"); + + + // ===== AUDIO FEATURES ===== + + /// + /// Audio speaker volume (VCP 0x62) + /// + public static VcpFeature AudioVolume => new(0x62, "volume", "Audio speaker volume", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, true, "vol"); + + /// + /// Speaker Select (VCP 0x63) + /// + public static VcpFeature SpeakerSelect => new(0x63, "speaker-select", "Speaker Select", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, false); + + /// + /// Audio: Microphone Volume (VCP 0x64) + /// + public static VcpFeature MicrophoneVolume => new(0x64, "microphone-volume", "Audio: Microphone Volume", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, true, "mic-volume"); + + /// + /// Audio mute/Screen blank (VCP 0x8D) + /// + public static VcpFeature AudioMute => new(0x8D, "mute", "Audio mute/Screen blank", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, false); + + /// + /// Audio treble (VCP 0x8F) + /// + public static VcpFeature AudioTreble => new(0x8F, "audio-treble", "Audio treble", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, true, "treble"); + + /// + /// Audio bass (VCP 0x91) + /// + public static VcpFeature AudioBass => new(0x91, "audio-bass", "Audio bass", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, true, "bass"); + + /// + /// Audio balance L/R (VCP 0x93) + /// + public static VcpFeature AudioBalance => new(0x93, "audio-balance", "Audio balance L/R", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, true, "balance"); + + /// + /// Audio processor mode (VCP 0x94) + /// + public static VcpFeature AudioProcessorMode => new(0x94, "audio-processor-mode", "Audio processor mode", VcpFeatureType.ReadWrite, VcpFeatureCategory.Audio, false); + + + /// + /// All features registry - contains all predefined MCCS features + /// + public static IReadOnlyList AllFeatures { get; } = new List + { + Degauss, + NewControlValue, + SoftControls, + RestoreDefaults, + RestoreBrightnessContrastDefaults, + RestoreGeometryDefaults, + RestoreColorDefaults, + RestoreTVDefaults, + ColorTemperatureIncrement, + ColorTemperatureRequest, + Clock, + Brightness, + FleshToneEnhancement, + Contrast, + Backlight, + SelectColorPreset, + RedGain, + UserColorVisionCompensation, + GreenGain, + BlueGain, + Focus, + AutoSetup, + AutoColorSetup, + HorizontalPosition, + HorizontalSize, + HorizontalPincushion, + HorizontalPincushionBalance, + HorizontalConvergenceRB, + HorizontalConvergenceMG, + HorizontalLinearity, + HorizontalLinearityBalance, + GrayScaleExpansion, + VerticalPosition, + VerticalSize, + VerticalPincushion, + VerticalPincushionBalance, + VerticalConvergenceRB, + VerticalConvergenceMG, + VerticalLinearity, + VerticalLinearityBalance, + ClockPhase, + HorizontalParallelogram, + VerticalParallelogram, + HorizontalKeystone, + VerticalKeystone, + Rotation, + TopCornerFlare, + TopCornerHook, + BottomCornerFlare, + BottomCornerHook, + ActiveControl, + PerformancePreservation, + HorizontalMoire, + VerticalMoire, + SixAxisSaturationRed, + SixAxisSaturationYellow, + SixAxisSaturationGreen, + SixAxisSaturationCyan, + SixAxisSaturationBlue, + SixAxisSaturationMagenta, + InputSource, + AudioVolume, + SpeakerSelect, + MicrophoneVolume, + AmbientLightSensor, + BacklightLevelWhite, + RedBlackLevel, + BacklightLevelRed, + GreenBlackLevel, + BacklightLevelGreen, + BlueBlackLevel, + BacklightLevelBlue, + Gamma, + LUTSize, + SinglePointLUTOperation, + BlockLUTOperation, + RemoteProcedureCall, + DisplayIdentificationOperation, + AdjustFocalPlane, + AdjustZoom, + Trapezoid, + Keystone, + HorizontalMirror, + VerticalMirror, + DisplayScaling, + Sharpness, + VelocityScanModulation, + ColorSaturation, + TVChannelUpDown, + TVSharpness, + AudioMute, + TVContrast, + AudioTreble, + Hue, + AudioBass, + TVBlackLevel, + AudioBalance, + AudioProcessorMode, + WindowPositionTLX, + WindowPositionTLY, + WindowPositionBRX, + WindowPositionBRY, + WindowControlOnOff, + WindowBackground, + SixAxisHueRed, + SixAxisHueYellow, + SixAxisHueGreen, + SixAxisHueCyan, + SixAxisHueBlue, + SixAxisHueMagenta, + AutoSetupOnOff, + WindowMaskControl, + ChangeSelectedWindow, + ScreenOrientation, + HorizontalFrequency, + VerticalFrequency, + Settings, + FlatPanelSubPixelLayout, + SourceTimingMode, + DisplayTechnologyType, + MonitorStatus, + PacketCount, + MonitorXOrigin, + MonitorYOrigin, + HeaderErrorCount, + BodyCRCErrorCount, + ClientID, + LinkControl, + DisplayUsageTime, + DisplayDescriptorLength, + TransmitDisplayDescriptor, + EnableDisplayOfDisplayDescriptor, + ApplicationEnableKey, + DisplayControllerType, + DisplayFirmwareLevel, + OSDButtonControl, + OSDLanguage, + StatusIndicators, + AuxiliaryDisplaySize, + AuxiliaryDisplayData, + OutputSelect, + AssetTag, + StereoVideoMode, + PowerMode, + AuxiliaryPowerOutput, + ScanMode, + ImageMode, + DisplayMode, + ScratchPad, + VcpVersion + }; +} diff --git a/DDCSwitch/VcpFeature.cs b/DDCSwitch/VcpFeature.cs new file mode 100644 index 0000000..74c19c1 --- /dev/null +++ b/DDCSwitch/VcpFeature.cs @@ -0,0 +1,121 @@ +namespace DDCSwitch; + +/// +/// Defines the access type for a VCP feature +/// +public enum VcpFeatureType +{ + /// + /// Feature can only be read + /// + ReadOnly, + + /// + /// Feature can only be written + /// + WriteOnly, + + /// + /// Feature can be both read and written + /// + ReadWrite +} + +/// +/// Defines the functional category of a VCP feature +/// +public enum VcpFeatureCategory +{ + /// + /// Image adjustment features (brightness, contrast, sharpness, etc.) + /// + ImageAdjustment, + + /// + /// Color control features (RGB gains, color temperature, etc.) + /// + ColorControl, + + /// + /// Geometry features (position, size, pincushion - mainly CRT) + /// + Geometry, + + /// + /// Audio features (volume, mute, balance, etc.) + /// + Audio, + + /// + /// Preset and factory features (restore defaults, degauss, etc.) + /// + Preset, + + /// + /// Miscellaneous features that don't fit other categories + /// + Miscellaneous +} + +/// +/// Represents a VCP (Virtual Control Panel) feature with its properties. +/// This is a partial class - feature definitions are in VcpFeature.Generated.cs +/// which is auto-generated from VcpFeatureData.json. +/// +public partial class VcpFeature +{ + public byte Code { get; } + public string Name { get; } + public string Description { get; } + public VcpFeatureType Type { get; } + public VcpFeatureCategory Category { get; } + public bool SupportsPercentage { get; } + public string[] Aliases { get; } + + public VcpFeature(byte code, string name, string description, VcpFeatureType type, VcpFeatureCategory category, bool supportsPercentage, params string[] aliases) + { + Code = code; + Name = name; + Description = description; + Type = type; + Category = category; + SupportsPercentage = supportsPercentage; + Aliases = aliases; + } + + // Legacy constructor for backward compatibility + public VcpFeature(byte code, string name, VcpFeatureType type, bool supportsPercentage) + : this(code, name, $"VCP feature {name} (0x{code:X2})", type, VcpFeatureCategory.Miscellaneous, supportsPercentage) + { + } + + public override string ToString() + { + return $"{Name} (0x{Code:X2})"; + } + + public override bool Equals(object? obj) + { + return obj is VcpFeature other && Code == other.Code; + } + + public override int GetHashCode() + { + return Code.GetHashCode(); + } +} + +/// +/// Information about a VCP feature discovered during scanning +/// +public record VcpFeatureInfo( + byte Code, + string Name, + string Description, + VcpFeatureType Type, + VcpFeatureCategory Category, + uint CurrentValue, + uint MaxValue, + bool IsSupported +); + diff --git a/DDCSwitch/VcpFeatureData.json b/DDCSwitch/VcpFeatureData.json new file mode 100644 index 0000000..e555c5e --- /dev/null +++ b/DDCSwitch/VcpFeatureData.json @@ -0,0 +1,1561 @@ +{ + "$schema": "vcp-feature-schema", + "description": "VCP (VESA Display Data Channel Command Interface) feature definitions from MCCS specification", + "features": [ + { + "code": "0x01", + "property": "Degauss", + "name": "degauss", + "description": "Degauss", + "type": "WriteOnly", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x02", + "property": "NewControlValue", + "name": "new-control-value", + "description": "New control value", + "type": "ReadWrite", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x03", + "property": "SoftControls", + "name": "soft-controls", + "description": "Soft controls", + "type": "ReadWrite", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x04", + "property": "RestoreDefaults", + "name": "restore-defaults", + "description": "Restore factory defaults", + "type": "WriteOnly", + "category": "Preset", + "supportsPercentage": false, + "aliases": [ + "factory-reset" + ] + }, + { + "code": "0x05", + "property": "RestoreBrightnessContrastDefaults", + "name": "restore-brightness-contrast", + "description": "Restore factory brightness/contrast defaults", + "type": "WriteOnly", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x06", + "property": "RestoreGeometryDefaults", + "name": "restore-geometry", + "description": "Restore factory geometry defaults", + "type": "WriteOnly", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x08", + "property": "RestoreColorDefaults", + "name": "restore-color", + "description": "Restore factory color defaults", + "type": "WriteOnly", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x0A", + "property": "RestoreTVDefaults", + "name": "restore-tv-defaults", + "description": "Restore factory TV defaults", + "type": "WriteOnly", + "category": "Preset", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x0B", + "property": "ColorTemperatureIncrement", + "name": "color-temp-increment", + "description": "Color temperature increment", + "type": "ReadOnly", + "category": "ColorControl", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x0C", + "property": "ColorTemperatureRequest", + "name": "color-temp-request", + "description": "Color temperature request", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": false, + "aliases": [ + "color-temp" + ] + }, + { + "code": "0x0E", + "property": "Clock", + "name": "clock", + "description": "Clock", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x10", + "property": "Brightness", + "name": "brightness", + "description": "Brightness control", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [ + "bright" + ] + }, + { + "code": "0x11", + "property": "FleshToneEnhancement", + "name": "flesh-tone-enhancement", + "description": "Flesh tone enhancement", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x12", + "property": "Contrast", + "name": "contrast", + "description": "Contrast control", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x13", + "property": "Backlight", + "name": "backlight", + "description": "Backlight control", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x14", + "property": "SelectColorPreset", + "name": "select-color-preset", + "description": "Select color preset", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [ + "color-preset" + ] + }, + { + "code": "0x16", + "property": "RedGain", + "name": "red-gain", + "description": "Video gain: Red", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [ + "red" + ] + }, + { + "code": "0x17", + "property": "UserColorVisionCompensation", + "name": "user-color-vision-compensation", + "description": "User color vision compensation", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x18", + "property": "GreenGain", + "name": "green-gain", + "description": "Video gain: Green", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [ + "green" + ] + }, + { + "code": "0x1A", + "property": "BlueGain", + "name": "blue-gain", + "description": "Video gain: Blue", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [ + "blue" + ] + }, + { + "code": "0x1C", + "property": "Focus", + "name": "focus", + "description": "Focus", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x1E", + "property": "AutoSetup", + "name": "auto-setup", + "description": "Auto setup", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x1F", + "property": "AutoColorSetup", + "name": "auto-color-setup", + "description": "Auto color setup", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x20", + "property": "HorizontalPosition", + "name": "h-position", + "description": "Horizontal Position (Phase)", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [ + "h-pos" + ] + }, + { + "code": "0x22", + "property": "HorizontalSize", + "name": "h-size", + "description": "Horizontal Size", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x24", + "property": "HorizontalPincushion", + "name": "h-pincushion", + "description": "Horizontal Pincushion", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x26", + "property": "HorizontalPincushionBalance", + "name": "h-pincushion-balance", + "description": "Horizontal Pincushion Balance", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x28", + "property": "HorizontalConvergenceRB", + "name": "h-convergence-rb", + "description": "Horizontal Convergence R/B", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x29", + "property": "HorizontalConvergenceMG", + "name": "h-convergence-mg", + "description": "Horizontal Convergence M/G", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x2A", + "property": "HorizontalLinearity", + "name": "h-linearity", + "description": "Horizontal Linearity", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x2C", + "property": "HorizontalLinearityBalance", + "name": "h-linearity-balance", + "description": "Horizontal Linearity Balance", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x2E", + "property": "GrayScaleExpansion", + "name": "gray-scale-expansion", + "description": "Gray scale expansion", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x30", + "property": "VerticalPosition", + "name": "v-position", + "description": "Vertical Position (Phase)", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [ + "v-pos" + ] + }, + { + "code": "0x32", + "property": "VerticalSize", + "name": "v-size", + "description": "Vertical Size", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x34", + "property": "VerticalPincushion", + "name": "v-pincushion", + "description": "Vertical Pincushion", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x36", + "property": "VerticalPincushionBalance", + "name": "v-pincushion-balance", + "description": "Vertical Pincushion Balance", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x38", + "property": "VerticalConvergenceRB", + "name": "v-convergence-rb", + "description": "Vertical Convergence R/B", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x39", + "property": "VerticalConvergenceMG", + "name": "v-convergence-mg", + "description": "Vertical Convergence M/G", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x3A", + "property": "VerticalLinearity", + "name": "v-linearity", + "description": "Vertical Linearity", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x3C", + "property": "VerticalLinearityBalance", + "name": "v-linearity-balance", + "description": "Vertical Linearity Balance", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x3E", + "property": "ClockPhase", + "name": "clock-phase", + "description": "Clock Phase", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [ + "phase" + ] + }, + { + "code": "0x40", + "property": "HorizontalParallelogram", + "name": "horizontal-parallelogram", + "description": "Horizontal Parallelogram", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x41", + "property": "VerticalParallelogram", + "name": "vertical-parallelogram", + "description": "Vertical Parallelogram", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x42", + "property": "HorizontalKeystone", + "name": "horizontal-keystone", + "description": "Horizontal Keystone", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x43", + "property": "VerticalKeystone", + "name": "vertical-keystone", + "description": "Vertical Keystone", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x44", + "property": "Rotation", + "name": "rotation", + "description": "Rotation", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x46", + "property": "TopCornerFlare", + "name": "top-corner-flare", + "description": "Top Corner Flare", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x48", + "property": "TopCornerHook", + "name": "top-corner-hook", + "description": "Top Corner Hook", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x4A", + "property": "BottomCornerFlare", + "name": "bottom-corner-flare", + "description": "Bottom Corner Flare", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x4C", + "property": "BottomCornerHook", + "name": "bottom-corner-hook", + "description": "Bottom Corner Hook", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x52", + "property": "ActiveControl", + "name": "active-control", + "description": "Active control", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x54", + "property": "PerformancePreservation", + "name": "performance-preservation", + "description": "Performance Preservation", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x56", + "property": "HorizontalMoire", + "name": "horizontal-moire", + "description": "Horizontal Moire", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x58", + "property": "VerticalMoire", + "name": "vertical-moire", + "description": "Vertical Moire", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x59", + "property": "SixAxisSaturationRed", + "name": "6-axis-saturation-red", + "description": "6 axis saturation: Red", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x5A", + "property": "SixAxisSaturationYellow", + "name": "6-axis-saturation-yellow", + "description": "6 axis saturation: Yellow", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x5B", + "property": "SixAxisSaturationGreen", + "name": "6-axis-saturation-green", + "description": "6 axis saturation: Green", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x5C", + "property": "SixAxisSaturationCyan", + "name": "6-axis-saturation-cyan", + "description": "6 axis saturation: Cyan", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x5D", + "property": "SixAxisSaturationBlue", + "name": "6-axis-saturation-blue", + "description": "6 axis saturation: Blue", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x5E", + "property": "SixAxisSaturationMagenta", + "name": "6-axis-saturation-magenta", + "description": "6 axis saturation: Magenta", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x60", + "property": "InputSource", + "name": "input", + "description": "Input source selection", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [ + "source" + ] + }, + { + "code": "0x62", + "property": "AudioVolume", + "name": "volume", + "description": "Audio speaker volume", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": true, + "aliases": [ + "vol" + ] + }, + { + "code": "0x63", + "property": "SpeakerSelect", + "name": "speaker-select", + "description": "Speaker Select", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x64", + "property": "MicrophoneVolume", + "name": "microphone-volume", + "description": "Audio: Microphone Volume", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": true, + "aliases": [ + "mic-volume" + ] + }, + { + "code": "0x66", + "property": "AmbientLightSensor", + "name": "ambient-light-sensor", + "description": "Ambient light sensor", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x6B", + "property": "BacklightLevelWhite", + "name": "backlight-level-white", + "description": "Backlight Level: White", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x6C", + "property": "RedBlackLevel", + "name": "red-black-level", + "description": "Video black level: Red", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x6D", + "property": "BacklightLevelRed", + "name": "backlight-level-red", + "description": "Backlight Level: Red", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x6E", + "property": "GreenBlackLevel", + "name": "green-black-level", + "description": "Video black level: Green", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x6F", + "property": "BacklightLevelGreen", + "name": "backlight-level-green", + "description": "Backlight Level: Green", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x70", + "property": "BlueBlackLevel", + "name": "blue-black-level", + "description": "Video black level: Blue", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x71", + "property": "BacklightLevelBlue", + "name": "backlight-level-blue", + "description": "Backlight Level: Blue", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x72", + "property": "Gamma", + "name": "gamma", + "description": "Gamma", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x73", + "property": "LUTSize", + "name": "lut-size", + "description": "LUT Size", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x74", + "property": "SinglePointLUTOperation", + "name": "single-point-lut-operation", + "description": "Single point LUT operation", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x75", + "property": "BlockLUTOperation", + "name": "block-lut-operation", + "description": "Block LUT operation", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x76", + "property": "RemoteProcedureCall", + "name": "remote-procedure-call", + "description": "Remote Procedure Call", + "type": "WriteOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x78", + "property": "DisplayIdentificationOperation", + "name": "display-identification-operation", + "description": "Display Identification Operation", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x7A", + "property": "AdjustFocalPlane", + "name": "adjust-focal-plane", + "description": "Adjust Focal Plane", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x7C", + "property": "AdjustZoom", + "name": "adjust-zoom", + "description": "Adjust Zoom", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x7E", + "property": "Trapezoid", + "name": "trapezoid", + "description": "Trapezoid", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x80", + "property": "Keystone", + "name": "keystone", + "description": "Keystone", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x82", + "property": "HorizontalMirror", + "name": "horizontal-mirror", + "description": "Horizontal Mirror (Flip)", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x84", + "property": "VerticalMirror", + "name": "vertical-mirror", + "description": "Vertical Mirror (Flip)", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x86", + "property": "DisplayScaling", + "name": "display-scaling", + "description": "Display Scaling", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x87", + "property": "Sharpness", + "name": "sharpness", + "description": "Sharpness control", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [ + "sharp" + ] + }, + { + "code": "0x88", + "property": "VelocityScanModulation", + "name": "velocity-scan-modulation", + "description": "Velocity Scan Modulation", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x8A", + "property": "ColorSaturation", + "name": "color-saturation", + "description": "Color Saturation", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [ + "saturation", + "sat" + ] + }, + { + "code": "0x8B", + "property": "TVChannelUpDown", + "name": "tv-channel-up-down", + "description": "TV Channel Up/Down", + "type": "WriteOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x8C", + "property": "TVSharpness", + "name": "tv-sharpness", + "description": "TV Sharpness", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x8D", + "property": "AudioMute", + "name": "mute", + "description": "Audio mute/Screen blank", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x8E", + "property": "TVContrast", + "name": "tv-contrast", + "description": "TV Contrast", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x8F", + "property": "AudioTreble", + "name": "audio-treble", + "description": "Audio treble", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": true, + "aliases": [ + "treble" + ] + }, + { + "code": "0x90", + "property": "Hue", + "name": "hue", + "description": "Hue", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x91", + "property": "AudioBass", + "name": "audio-bass", + "description": "Audio bass", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": true, + "aliases": [ + "bass" + ] + }, + { + "code": "0x92", + "property": "TVBlackLevel", + "name": "tv-black-level", + "description": "TV Black level/Luminesence", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x93", + "property": "AudioBalance", + "name": "audio-balance", + "description": "Audio balance L/R", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": true, + "aliases": [ + "balance" + ] + }, + { + "code": "0x94", + "property": "AudioProcessorMode", + "name": "audio-processor-mode", + "description": "Audio processor mode", + "type": "ReadWrite", + "category": "Audio", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x95", + "property": "WindowPositionTLX", + "name": "window-position-tl-x", + "description": "Window Position(TL_X)", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x96", + "property": "WindowPositionTLY", + "name": "window-position-tl-y", + "description": "Window Position(TL_Y)", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x97", + "property": "WindowPositionBRX", + "name": "window-position-br-x", + "description": "Window Position(BR_X)", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x98", + "property": "WindowPositionBRY", + "name": "window-position-br-y", + "description": "Window Position(BR_Y)", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x99", + "property": "WindowControlOnOff", + "name": "window-control-on-off", + "description": "Window control on/off", + "type": "ReadWrite", + "category": "Geometry", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0x9A", + "property": "WindowBackground", + "name": "window-background", + "description": "Window background", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x9B", + "property": "SixAxisHueRed", + "name": "6-axis-hue-red", + "description": "6 axis hue control: Red", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x9C", + "property": "SixAxisHueYellow", + "name": "6-axis-hue-yellow", + "description": "6 axis hue control: Yellow", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x9D", + "property": "SixAxisHueGreen", + "name": "6-axis-hue-green", + "description": "6 axis hue control: Green", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x9E", + "property": "SixAxisHueCyan", + "name": "6-axis-hue-cyan", + "description": "6 axis hue control: Cyan", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0x9F", + "property": "SixAxisHueBlue", + "name": "6-axis-hue-blue", + "description": "6 axis hue control: Blue", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0xA0", + "property": "SixAxisHueMagenta", + "name": "6-axis-hue-magenta", + "description": "6 axis hue control: Magenta", + "type": "ReadWrite", + "category": "ColorControl", + "supportsPercentage": true, + "aliases": [] + }, + { + "code": "0xA2", + "property": "AutoSetupOnOff", + "name": "auto-setup-on-off", + "description": "Auto setup on/off", + "type": "WriteOnly", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xA4", + "property": "WindowMaskControl", + "name": "window-mask-control", + "description": "Window mask control", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xA5", + "property": "ChangeSelectedWindow", + "name": "change-selected-window", + "description": "Change the selected window", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xAA", + "property": "ScreenOrientation", + "name": "screen-orientation", + "description": "Screen Orientation", + "type": "ReadOnly", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [ + "orientation" + ] + }, + { + "code": "0xAC", + "property": "HorizontalFrequency", + "name": "horizontal-frequency", + "description": "Horizontal frequency", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "h-freq" + ] + }, + { + "code": "0xAE", + "property": "VerticalFrequency", + "name": "vertical-frequency", + "description": "Vertical frequency", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "v-freq" + ] + }, + { + "code": "0xB0", + "property": "Settings", + "name": "settings", + "description": "Settings", + "type": "WriteOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xB2", + "property": "FlatPanelSubPixelLayout", + "name": "flat-panel-sub-pixel-layout", + "description": "Flat panel sub-pixel layout", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xB4", + "property": "SourceTimingMode", + "name": "source-timing-mode", + "description": "Source Timing Mode", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xB6", + "property": "DisplayTechnologyType", + "name": "display-technology-type", + "description": "Display technology type", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xB7", + "property": "MonitorStatus", + "name": "monitor-status", + "description": "Monitor status", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xB8", + "property": "PacketCount", + "name": "packet-count", + "description": "Packet count", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xB9", + "property": "MonitorXOrigin", + "name": "monitor-x-origin", + "description": "Monitor X origin", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xBA", + "property": "MonitorYOrigin", + "name": "monitor-y-origin", + "description": "Monitor Y origin", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xBB", + "property": "HeaderErrorCount", + "name": "header-error-count", + "description": "Header error count", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xBC", + "property": "BodyCRCErrorCount", + "name": "body-crc-error-count", + "description": "Body CRC error count", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xBD", + "property": "ClientID", + "name": "client-id", + "description": "Client ID", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xBE", + "property": "LinkControl", + "name": "link-control", + "description": "Link control", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC0", + "property": "DisplayUsageTime", + "name": "display-usage-time", + "description": "Display usage time", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC2", + "property": "DisplayDescriptorLength", + "name": "display-descriptor-length", + "description": "Display descriptor length", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC3", + "property": "TransmitDisplayDescriptor", + "name": "transmit-display-descriptor", + "description": "Transmit display descriptor", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC4", + "property": "EnableDisplayOfDisplayDescriptor", + "name": "enable-display-of-display-descriptor", + "description": "Enable display of 'display descriptor'", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC6", + "property": "ApplicationEnableKey", + "name": "application-enable-key", + "description": "Application enable key", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC8", + "property": "DisplayControllerType", + "name": "display-controller-type", + "description": "Display controller type", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xC9", + "property": "DisplayFirmwareLevel", + "name": "display-firmware-level", + "description": "Display firmware level", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "firmware" + ] + }, + { + "code": "0xCA", + "property": "OSDButtonControl", + "name": "osd-button-control", + "description": "OSD/Button Control", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "osd" + ] + }, + { + "code": "0xCC", + "property": "OSDLanguage", + "name": "osd-language", + "description": "OSD Language", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "language" + ] + }, + { + "code": "0xCD", + "property": "StatusIndicators", + "name": "status-indicators", + "description": "Status Indicators", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xCE", + "property": "AuxiliaryDisplaySize", + "name": "auxiliary-display-size", + "description": "Auxiliary display size", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xCF", + "property": "AuxiliaryDisplayData", + "name": "auxiliary-display-data", + "description": "Auxiliary display data", + "type": "WriteOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xD0", + "property": "OutputSelect", + "name": "output-select", + "description": "Output select", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xD2", + "property": "AssetTag", + "name": "asset-tag", + "description": "Asset Tag", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xD4", + "property": "StereoVideoMode", + "name": "stereo-video-mode", + "description": "Stereo video mode", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xD6", + "property": "PowerMode", + "name": "power-mode", + "description": "Power mode", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "power" + ] + }, + { + "code": "0xD7", + "property": "AuxiliaryPowerOutput", + "name": "auxiliary-power-output", + "description": "Auxiliary power output", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xDA", + "property": "ScanMode", + "name": "scan-mode", + "description": "Scan mode", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xDB", + "property": "ImageMode", + "name": "image-mode", + "description": "Image Mode", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [ + "mode" + ] + }, + { + "code": "0xDC", + "property": "DisplayMode", + "name": "display-mode", + "description": "Display Mode", + "type": "ReadWrite", + "category": "ImageAdjustment", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xDE", + "property": "ScratchPad", + "name": "scratch-pad", + "description": "Scratch Pad", + "type": "ReadWrite", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [] + }, + { + "code": "0xDF", + "property": "VcpVersion", + "name": "vcp-version", + "description": "VCP Version", + "type": "ReadOnly", + "category": "Miscellaneous", + "supportsPercentage": false, + "aliases": [ + "version" + ] + } + ] +} \ No newline at end of file diff --git a/EXAMPLES.md b/EXAMPLES.md index 4dd0e27..d013532 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,72 +1,474 @@ -# DDCSwitch Examples +# ddcswitch Examples -This document contains detailed examples and use cases for DDCSwitch. +This document contains detailed examples and use cases for ddcswitch, including input switching, brightness/contrast control, comprehensive VCP feature access, and automation. + +## 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 +``` + +### Color Control Examples + +Control RGB gains for color calibration: + +```powershell +# Set individual RGB gains (percentage values) +ddcswitch set 0 red-gain 95% +ddcswitch set 0 green-gain 90% +ddcswitch set 0 blue-gain 85% + +# Get current RGB values +ddcswitch get 0 red-gain +ddcswitch get 0 green-gain +ddcswitch get 0 blue-gain + +# Color temperature control (if supported) +ddcswitch set 0 color-temp-request 6500 +ddcswitch get 0 color-temp-request + +# Gamma control +ddcswitch set 0 gamma 2.2 +ddcswitch get 0 gamma + +# Hue and saturation +ddcswitch set 0 hue 50% +ddcswitch set 0 saturation 80% +``` + +### Audio Control Examples + +Control monitor speakers (if supported): + +```powershell +# Volume control (percentage) +ddcswitch set 0 volume 75% +ddcswitch get 0 volume + +# Mute/unmute +ddcswitch set 0 mute 1 # Mute +ddcswitch set 0 mute 0 # Unmute + +# Audio balance (if supported) +ddcswitch set 0 audio-balance 50% # Centered + +# Treble and bass (if supported) +ddcswitch set 0 audio-treble 60% +ddcswitch set 0 audio-bass 70% +``` + +### Advanced Image Controls + +Beyond basic brightness and contrast: + +```powershell +# Sharpness control +ddcswitch set 0 sharpness 75% +ddcswitch get 0 sharpness + +# Backlight control (LED monitors) +ddcswitch set 0 backlight 80% +ddcswitch get 0 backlight + +# Image orientation (if supported) +ddcswitch set 0 image-orientation 0 # Normal +ddcswitch set 0 image-orientation 1 # 90 rotation + +# Image mode presets (if supported) +ddcswitch set 0 image-mode 1 # Standard +ddcswitch set 0 image-mode 2 # Movie +ddcswitch set 0 image-mode 3 # Game +``` + +### Factory Reset and Calibration + +```powershell +# Restore all factory defaults +ddcswitch set 0 restore-defaults 1 + +# Restore specific defaults +ddcswitch set 0 restore-brightness-contrast 1 +ddcswitch set 0 restore-color 1 +ddcswitch set 0 restore-geometry 1 + +# Degauss (CRT monitors) +ddcswitch set 0 degauss 1 + +# Auto calibration features (if supported) +ddcswitch set 0 auto-color-setup 1 +ddcswitch set 0 auto-size-center 1 +``` + +### Complete Monitor Profile Examples + +Create comprehensive monitor profiles using all available features: + +```powershell +# gaming-profile-advanced.ps1 +Write-Host "Activating Advanced Gaming Profile..." -ForegroundColor Cyan + +# Input and basic settings +ddcswitch set 0 input HDMI1 +ddcswitch set 0 brightness 90% +ddcswitch set 0 contrast 85% + +# Color optimization for gaming +ddcswitch set 0 red-gain 100% +ddcswitch set 0 green-gain 95% +ddcswitch set 0 blue-gain 90% +ddcswitch set 0 saturation 110% # Enhanced colors +ddcswitch set 0 sharpness 80% # Crisp details + +# Audio settings +ddcswitch set 0 volume 60% +ddcswitch set 0 mute 0 + +Write-Host "Advanced Gaming Profile Activated!" -ForegroundColor Green +``` + +```powershell +# work-profile-advanced.ps1 +Write-Host "Activating Advanced Work Profile..." -ForegroundColor Cyan + +# Input and basic settings +ddcswitch set 0 input DP1 +ddcswitch set 0 brightness 60% +ddcswitch set 0 contrast 75% + +# Color optimization for text work +ddcswitch set 0 red-gain 85% +ddcswitch set 0 green-gain 90% +ddcswitch set 0 blue-gain 95% +ddcswitch set 0 saturation 70% # Reduced saturation for comfort +ddcswitch set 0 sharpness 60% # Softer for long reading + +# Audio settings +ddcswitch set 0 volume 40% # Lower for office environment +ddcswitch set 0 mute 0 + +Write-Host "Advanced Work Profile Activated!" -ForegroundColor Green +``` + +```powershell +# photo-editing-profile.ps1 +Write-Host "Activating Photo Editing Profile..." -ForegroundColor Cyan + +# Input and basic settings +ddcswitch set 0 input DP1 +ddcswitch set 0 brightness 70% +ddcswitch set 0 contrast 80% + +# Accurate color reproduction +ddcswitch set 0 red-gain 90% +ddcswitch set 0 green-gain 90% +ddcswitch set 0 blue-gain 90% +ddcswitch set 0 saturation 100% # Natural saturation +ddcswitch set 0 gamma 2.2 # Standard gamma +ddcswitch set 0 color-temp-request 6500 # D65 standard + +# Disable audio to avoid distractions +ddcswitch set 0 mute 1 + +Write-Host "Photo Editing Profile Activated!" -ForegroundColor Green +``` ## Basic Usage Examples ### Check What Monitors Support DDC/CI ```powershell -DDCSwitch list +ddcswitch list ``` This will show all your monitors and indicate which ones support DDC/CI control. Monitors with "OK" status can be controlled. -### Get Current Input of Primary Monitor +### Verbose Monitor Information + +Get detailed information including brightness and contrast: + +```powershell +ddcswitch list --verbose +``` + +This shows current brightness and contrast levels for each monitor (displays "N/A" for unsupported features). + +### Get Current Settings + +```powershell +# Get all VCP features for primary monitor (scans all supported features) +ddcswitch get 0 + +# Get all features by monitor name (partial matching supported) +ddcswitch get "VG270U P" +ddcswitch get "Generic PnP" + +# Get specific features by index +ddcswitch get 0 input # Current input source +ddcswitch get 0 brightness # Current brightness +ddcswitch get 0 contrast # Current contrast + +# Get specific features by monitor name +ddcswitch get "Generic PnP" input +ddcswitch get "LG" brightness + +# Get raw VCP value +ddcswitch get 0 0x10 # Brightness (raw) +``` + +### Set Monitor Settings + +```powershell +# Switch input +ddcswitch set 0 HDMI1 + +# Set brightness to 75% +ddcswitch set 0 brightness 75% + +# Set contrast to 80% +ddcswitch set 0 contrast 80% + +# Set raw VCP value +ddcswitch set 0 0x10 120 # Brightness (raw value) +``` + +## Brightness and Contrast Control + +### Basic Brightness Control + +```powershell +# Set brightness to specific percentage +ddcswitch set 0 brightness 50% +ddcswitch set 0 brightness 75% +ddcswitch set 0 brightness 100% + +# Get current brightness +ddcswitch get 0 brightness +# Output: Monitor: Generic PnP Monitor / Brightness: 75% (120/160) +``` + +### Basic Contrast Control + +```powershell +# Set contrast to specific percentage +ddcswitch set 0 contrast 60% +ddcswitch set 0 contrast 85% +ddcswitch set 0 contrast 100% + +# Get current contrast +ddcswitch get 0 contrast +# Output: Monitor: Generic PnP Monitor / Contrast: 85% (136/160) +``` + +### Brightness Presets + +Create quick brightness presets: + +```powershell +# brightness-low.ps1 +ddcswitch set 0 brightness 25% +Write-Host "Brightness set to 25% (Low)" -ForegroundColor Green + +# brightness-medium.ps1 +ddcswitch set 0 brightness 50% +Write-Host "Brightness set to 50% (Medium)" -ForegroundColor Green + +# brightness-high.ps1 +ddcswitch set 0 brightness 75% +Write-Host "Brightness set to 75% (High)" -ForegroundColor Green + +# brightness-max.ps1 +ddcswitch set 0 brightness 100% +Write-Host "Brightness set to 100% (Maximum)" -ForegroundColor Green +``` + +### Time-Based Brightness Control + +Automatically adjust brightness based on time of day: + +```powershell +# auto-brightness.ps1 +$hour = (Get-Date).Hour + +if ($hour -ge 6 -and $hour -lt 9) { + # Morning: Medium brightness + ddcswitch set 0 brightness 60% + Write-Host "Morning brightness: 60%" -ForegroundColor Yellow +} elseif ($hour -ge 9 -and $hour -lt 18) { + # Daytime: High brightness + ddcswitch set 0 brightness 85% + Write-Host "Daytime brightness: 85%" -ForegroundColor Green +} elseif ($hour -ge 18 -and $hour -lt 22) { + # Evening: Medium brightness + ddcswitch set 0 brightness 50% + Write-Host "Evening brightness: 50%" -ForegroundColor Orange +} else { + # Night: Low brightness + ddcswitch set 0 brightness 25% + Write-Host "Night brightness: 25%" -ForegroundColor Blue +} +``` + +### Gaming vs Work Profiles + +Create different brightness/contrast profiles: + +```powershell +# gaming-profile.ps1 +Write-Host "Activating Gaming Profile..." -ForegroundColor Cyan +ddcswitch set 0 input HDMI1 # Switch to console +ddcswitch set 0 brightness 90% # High brightness for gaming +ddcswitch set 0 contrast 85% # High contrast for visibility +Write-Host "Gaming profile activated!" -ForegroundColor Green + +# work-profile.ps1 +Write-Host "Activating Work Profile..." -ForegroundColor Cyan +ddcswitch set 0 input DP1 # Switch to PC +ddcswitch set 0 brightness 60% # Comfortable brightness for long work +ddcswitch set 0 contrast 75% # Balanced contrast for text +Write-Host "Work profile activated!" -ForegroundColor Green +``` + +## Raw VCP Access Examples + +### Discover VCP Features + +```powershell +# Scan all VCP features for all monitors +ddcswitch get all + +# Scan all VCP features for a specific monitor +ddcswitch get 0 + +# Scan by monitor name +ddcswitch get "VG270U P" +``` + +### Common VCP Codes ```powershell -# If primary monitor is index 0 -DDCSwitch get 0 +# Brightness (VCP 0x10) +ddcswitch get 0 0x10 +ddcswitch set 0 0x10 120 + +# Contrast (VCP 0x12) +ddcswitch get 0 0x12 +ddcswitch set 0 0x12 140 + +# Input Source (VCP 0x60) +ddcswitch get 0 0x60 +ddcswitch set 0 0x60 0x11 # HDMI1 + +# Color Temperature (VCP 0x14) - if supported +ddcswitch get 0 0x14 +ddcswitch set 0 0x14 6500 # 6500K ``` -### Switch Between Two Inputs Quickly +### Test Unknown VCP Codes ```powershell -# Switch to console (HDMI) -DDCSwitch set 0 HDMI1 +# test-vcp-codes.ps1 - Discover what VCP codes your monitor supports +Write-Host "Testing VCP codes for Monitor 0" -ForegroundColor Cyan + +$commonCodes = @(0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x20, 0x30, 0x60, 0x62, 0x6C, 0x6E, 0x70) -# Switch to PC (DisplayPort) -DDCSwitch set 0 DP1 +foreach ($code in $commonCodes) { + $hexCode = "0x{0:X2}" -f $code + try { + $result = ddcswitch get 0 $hexCode 2>$null + if ($result -notmatch "error|failed") { + Write-Host "? VCP $hexCode supported: $result" -ForegroundColor Green + } + } catch { + Write-Host "? VCP $hexCode not supported" -ForegroundColor Red + } +} ``` ## JSON Output and Automation -All DDCSwitch commands support the `--json` flag for machine-readable output. This is perfect for scripting, automation, and integration with other tools. +All ddcswitch commands support the `--json` flag for machine-readable output. This is perfect for scripting, automation, and integration with other tools. ### PowerShell JSON Examples -#### Example 1: Conditional Input Switching +#### Example 1: Conditional Input Switching with Brightness Control -Check the current input and switch only if needed: +Check the current input and switch only if needed, then adjust brightness: ```powershell -# Check if monitor is on HDMI1, switch to DP1 if not -$result = DDCSwitch get 0 --json | ConvertFrom-Json +# Check if monitor is on HDMI1, switch to DP1 if not, then set work brightness +$result = ddcswitch get 0 --json | ConvertFrom-Json if ($result.success -and $result.currentInputCode -ne "0x11") { Write-Host "Monitor is on $($result.currentInput), switching to HDMI1..." - DDCSwitch set 0 HDMI1 --json | Out-Null + ddcswitch set 0 HDMI1 --json | Out-Null + ddcswitch set 0 brightness 75% --json | Out-Null + Write-Host "Switched to HDMI1 and set brightness to 75%" -ForegroundColor Green } else { Write-Host "Monitor already on HDMI1" } ``` -#### Example 2: Switch All Monitors with Error Handling +#### Example 2: Complete Monitor Setup with All Features ```powershell -# Switch all available monitors to HDMI1 with error handling -$listResult = DDCSwitch list --json | ConvertFrom-Json +# Switch all available monitors with full configuration +$listResult = ddcswitch list --json | ConvertFrom-Json if ($listResult.success) { foreach ($monitor in $listResult.monitors) { if ($monitor.status -eq "ok") { - Write-Host "Switching $($monitor.name) to HDMI1..." - $setResult = DDCSwitch set $monitor.index HDMI1 --json | ConvertFrom-Json + Write-Host "Configuring $($monitor.name)..." -ForegroundColor Cyan + # Set input + $setResult = ddcswitch set $monitor.index HDMI1 --json | ConvertFrom-Json if ($setResult.success) { - Write-Host "✓ Successfully switched $($monitor.name)" -ForegroundColor Green + Write-Host " ? Input: HDMI1" -ForegroundColor Green + } + + # Set brightness + $brightnessResult = ddcswitch set $monitor.index brightness 75% --json | ConvertFrom-Json + if ($brightnessResult.success) { + Write-Host " ? Brightness: 75%" -ForegroundColor Green } else { - Write-Host "✗ Failed: $($setResult.error)" -ForegroundColor Red + Write-Host " ? Brightness not supported" -ForegroundColor Yellow + } + + # Set contrast + $contrastResult = ddcswitch set $monitor.index contrast 80% --json | ConvertFrom-Json + if ($contrastResult.success) { + Write-Host " ? Contrast: 80%" -ForegroundColor Green + } else { + Write-Host " ? Contrast not supported" -ForegroundColor Yellow } } } @@ -75,13 +477,13 @@ if ($listResult.success) { } ``` -#### Example 3: Monitor Status Dashboard +#### Example 3: Monitor Status Dashboard with VCP Features -Create a simple dashboard showing all monitor states: +Create a comprehensive dashboard showing all monitor states: ```powershell # monitor-dashboard.ps1 -$result = DDCSwitch list --json | ConvertFrom-Json +$result = ddcswitch list --verbose --json | ConvertFrom-Json if ($result.success) { Write-Host "`n=== Monitor Status Dashboard ===" -ForegroundColor Cyan @@ -93,32 +495,55 @@ if ($result.success) { Write-Host " Device: $($monitor.deviceName)" Write-Host " Input: $($monitor.currentInput) ($($monitor.currentInputCode))" Write-Host " Status: $($monitor.status.ToUpper())" + + if ($monitor.brightness) { + Write-Host " Brightness: $($monitor.brightness)" -ForegroundColor Green + } else { + Write-Host " Brightness: N/A" -ForegroundColor Gray + } + + if ($monitor.contrast) { + Write-Host " Contrast: $($monitor.contrast)" -ForegroundColor Green + } else { + Write-Host " Contrast: N/A" -ForegroundColor Gray + } Write-Host "" } } ``` -#### Example 4: Toggle Between Two Inputs +#### Example 4: Smart Brightness Toggle ```powershell -# toggle-input.ps1 +# smart-brightness-toggle.ps1 param([int]$MonitorIndex = 0) -$result = DDCSwitch get $MonitorIndex --json | ConvertFrom-Json +$result = ddcswitch get $MonitorIndex brightness --json | ConvertFrom-Json if ($result.success) { - $newInput = if ($result.currentInputCode -eq "0x11") { "DP1" } else { "HDMI1" } - $switchResult = DDCSwitch set $MonitorIndex $newInput --json | ConvertFrom-Json + $currentPercent = $result.percentageValue + + # Toggle between 25%, 50%, 75%, 100% + $newBrightness = switch ($currentPercent) { + {$_ -le 25} { 50 } + {$_ -le 50} { 75 } + {$_ -le 75} { 100 } + default { 25 } + } + + $switchResult = ddcswitch set $MonitorIndex brightness "$newBrightness%" --json | ConvertFrom-Json if ($switchResult.success) { - Write-Host "Toggled from $($result.currentInput) to $($switchResult.newInput)" -ForegroundColor Green + Write-Host "Brightness: $currentPercent% ? $newBrightness%" -ForegroundColor Green } +} else { + Write-Host "Brightness control not supported on this monitor" -ForegroundColor Yellow } ``` ### Python JSON Examples -#### Example 1: Simple Monitor Switcher +#### Example 1: Monitor Control with Brightness/Contrast ```python #!/usr/bin/env python3 @@ -127,7 +552,7 @@ import json import sys def run_ddc(args): - """Run DDCSwitch and return JSON result""" + """Run ddcswitch and return JSON result""" result = subprocess.run( ['DDCSwitch'] + args + ['--json'], capture_output=True, @@ -136,146 +561,215 @@ def run_ddc(args): return json.loads(result.stdout) def list_monitors(): - """List all monitors""" - data = run_ddc(['list']) + """List all monitors with brightness/contrast info""" + data = run_ddc(['list', '--verbose']) if data['success']: for monitor in data['monitors']: primary = " [PRIMARY]" if monitor['isPrimary'] else "" print(f"{monitor['index']}: {monitor['name']}{primary}") - print(f" Current: {monitor['currentInput']} ({monitor['currentInputCode']})") + print(f" Input: {monitor['currentInput']} ({monitor['currentInputCode']})") + print(f" Brightness: {monitor.get('brightness', 'N/A')}") + print(f" Contrast: {monitor.get('contrast', 'N/A')}") else: print(f"Error: {data['error']}", file=sys.stderr) +def set_brightness(monitor_index, percentage): + """Set monitor brightness""" + data = run_ddc(['set', str(monitor_index), 'brightness', f'{percentage}%']) + if data['success']: + print(f"? Set brightness to {percentage}% on {data['monitor']['name']}") + else: + print(f"? Error: {data['error']}", file=sys.stderr) + +def set_contrast(monitor_index, percentage): + """Set monitor contrast""" + data = run_ddc(['set', str(monitor_index), 'contrast', f'{percentage}%']) + if data['success']: + print(f"? Set contrast to {percentage}% on {data['monitor']['name']}") + else: + print(f"? Error: {data['error']}", file=sys.stderr) + def switch_input(monitor_index, input_name): """Switch monitor input""" data = run_ddc(['set', str(monitor_index), input_name]) if data['success']: - print(f"✓ Switched {data['monitor']['name']} to {data['newInput']}") + print(f"? Switched {data['monitor']['name']} to {data['newInput']}") else: - print(f"✗ Error: {data['error']}", file=sys.stderr) + print(f"? Error: {data['error']}", file=sys.stderr) sys.exit(1) # Example usage if __name__ == '__main__': list_monitors() + # set_brightness(0, 75) + # set_contrast(0, 80) # switch_input(0, 'HDMI1') ``` -#### Example 2: Gaming Mode Automation +#### Example 2: Automated Brightness Control ```python #!/usr/bin/env python3 """ -Automatically switch monitors based on running applications -Usage: python gaming_mode.py +Automatically adjust monitor brightness based on time of day +Usage: python auto_brightness.py """ import subprocess import json import time -import psutil +from datetime import datetime def run_ddc(args): result = subprocess.run(['DDCSwitch'] + args + ['--json'], capture_output=True, text=True) return json.loads(result.stdout) -def switch_to_gaming(): - """Switch all monitors to HDMI (console inputs)""" - print("🎮 Activating gaming mode...") +def set_brightness_all(percentage): + """Set brightness on all supported monitors""" + print(f"?? Setting brightness to {percentage}%...") data = run_ddc(['list']) if data['success']: for monitor in data['monitors']: if monitor['status'] == 'ok': - result = run_ddc(['set', str(monitor['index']), 'HDMI1']) + result = run_ddc(['set', str(monitor['index']), 'brightness', f'{percentage}%']) if result['success']: - print(f" ✓ {monitor['name']} → HDMI1") + print(f" ? {monitor['name']} ? {percentage}%") + else: + print(f" ? {monitor['name']} ? Brightness not supported") -def switch_to_work(): - """Switch all monitors to DisplayPort (PC inputs)""" - print("💼 Activating work mode...") +def get_brightness_for_time(): + """Get appropriate brightness based on current time""" + hour = datetime.now().hour + + if 6 <= hour < 9: # Morning + return 60 + elif 9 <= hour < 18: # Daytime + return 85 + elif 18 <= hour < 22: # Evening + return 50 + else: # Night + return 25 + +def gaming_mode(): + """Switch to gaming setup with high brightness""" + print("?? Activating gaming mode...") data = run_ddc(['list']) if data['success']: for monitor in data['monitors']: if monitor['status'] == 'ok': - result = run_ddc(['set', str(monitor['index']), 'DP1']) - if result['success']: - print(f" ✓ {monitor['name']} → DP1") - -def is_game_running(): - """Check if specific games are running""" - game_processes = ['steam.exe', 'EpicGamesLauncher.exe'] - for proc in psutil.process_iter(['name']): - if proc.info['name'] in game_processes: - return True - return False - -# Monitor and switch automatically -if __name__ == '__main__': - previous_state = None + # Switch to HDMI (console) + input_result = run_ddc(['set', str(monitor['index']), 'HDMI1']) + if input_result['success']: + print(f" ? {monitor['name']} ? HDMI1") + + # Set high brightness for gaming + brightness_result = run_ddc(['set', str(monitor['index']), 'brightness', '90%']) + if brightness_result['success']: + print(f" ? {monitor['name']} ? 90% brightness") + +def work_mode(): + """Switch to work setup with comfortable brightness""" + print("?? Activating work mode...") + data = run_ddc(['list']) - while True: - gaming = is_game_running() - - if gaming != previous_state: - if gaming: - switch_to_gaming() - else: - switch_to_work() - previous_state = gaming - - time.sleep(10) # Check every 10 seconds + if data['success']: + for monitor in data['monitors']: + if monitor['status'] == 'ok': + # Switch to DisplayPort (PC) + input_result = run_ddc(['set', str(monitor['index']), 'DP1']) + if input_result['success']: + print(f" ? {monitor['name']} ? DP1") + + # Set comfortable brightness for work + brightness_result = run_ddc(['set', str(monitor['index']), 'brightness', '60%']) + if brightness_result['success']: + print(f" ? {monitor['name']} ? 60% brightness") + +# Auto-brightness based on time +if __name__ == '__main__': + brightness = get_brightness_for_time() + set_brightness_all(brightness) ``` ### Node.js JSON Examples -#### Example 1: Monitor Information API +#### Example 1: Monitor Control API with VCP Features ```javascript // monitor-api.js const { execSync } = require('child_process'); -class DDCSwitch { +class ddcswitch { static exec(args) { - const output = execSync(`DDCSwitch ${args.join(' ')} --json`, { + const output = execSync(`ddcswitch ${args.join(' ')} --json`, { encoding: 'utf-8' }); return JSON.parse(output); } - static listMonitors() { - return this.exec(['list']); + static listMonitors(verbose = false) { + const args = verbose ? ['list', '--verbose'] : ['list']; + return this.exec(args); } static getCurrentInput(monitorIndex) { return this.exec(['get', monitorIndex]); } + static getBrightness(monitorIndex) { + return this.exec(['get', monitorIndex, 'brightness']); + } + + static getContrast(monitorIndex) { + return this.exec(['get', monitorIndex, 'contrast']); + } + static setInput(monitorIndex, input) { return this.exec(['set', monitorIndex, input]); } + + static setBrightness(monitorIndex, percentage) { + return this.exec(['set', monitorIndex, 'brightness', `${percentage}%`]); + } + + static setContrast(monitorIndex, percentage) { + return this.exec(['set', monitorIndex, 'contrast', `${percentage}%`]); + } + + static setRawVcp(monitorIndex, vcpCode, value) { + return this.exec(['set', monitorIndex, vcpCode, value]); + } } // Usage example -const monitors = DDCSwitch.listMonitors(); +const monitors = DDCSwitch.listMonitors(true); console.log(`Found ${monitors.monitors.length} monitors`); monitors.monitors.forEach(monitor => { - console.log(`${monitor.index}: ${monitor.name} - ${monitor.currentInput}`); + console.log(`${monitor.index}: ${monitor.name}`); + console.log(` Input: ${monitor.currentInput}`); + console.log(` Brightness: ${monitor.brightness || 'N/A'}`); + console.log(` Contrast: ${monitor.contrast || 'N/A'}`); }); -// Switch first monitor to HDMI1 -const result = DDCSwitch.setInput(0, 'HDMI1'); -if (result.success) { - console.log(`✓ Switched to ${result.newInput}`); +// Set brightness and contrast +const brightnessResult = DDCSwitch.setBrightness(0, 75); +if (brightnessResult.success) { + console.log(`? Set brightness to 75%`); +} + +const contrastResult = DDCSwitch.setContrast(0, 80); +if (contrastResult.success) { + console.log(`? Set contrast to 80%`); } ``` -#### Example 2: Express.js REST API +#### Example 2: Express.js REST API with VCP Support ```javascript -// server.js - Web API for monitor control +// server.js - Web API for complete monitor control const express = require('express'); const { execSync } = require('child_process'); @@ -284,7 +778,7 @@ app.use(express.json()); function runDDC(args) { try { - const output = execSync(`DDCSwitch ${args.join(' ')} --json`, { + const output = execSync(`ddcswitch ${args.join(' ')} --json`, { encoding: 'utf-8' }); return JSON.parse(output); @@ -293,18 +787,32 @@ function runDDC(args) { } } -// GET /monitors - List all monitors +// GET /monitors - List all monitors (with verbose option) app.get('/monitors', (req, res) => { - const result = runDDC(['list']); + const verbose = req.query.verbose === 'true'; + const args = verbose ? ['list', '--verbose'] : ['list']; + const result = runDDC(args); res.json(result); }); -// GET /monitors/:id - Get specific monitor +// GET /monitors/:id - Get specific monitor info app.get('/monitors/:id', (req, res) => { const result = runDDC(['get', req.params.id]); res.json(result); }); +// GET /monitors/:id/brightness - Get brightness +app.get('/monitors/:id/brightness', (req, res) => { + const result = runDDC(['get', req.params.id, 'brightness']); + res.json(result); +}); + +// GET /monitors/:id/contrast - Get contrast +app.get('/monitors/:id/contrast', (req, res) => { + const result = runDDC(['get', req.params.id, 'contrast']); + res.json(result); +}); + // POST /monitors/:id/input - Set monitor input app.post('/monitors/:id/input', (req, res) => { const { input } = req.body; @@ -312,8 +820,52 @@ app.post('/monitors/:id/input', (req, res) => { res.json(result); }); +// POST /monitors/:id/brightness - Set brightness +app.post('/monitors/:id/brightness', (req, res) => { + const { percentage } = req.body; + const result = runDDC(['set', req.params.id, 'brightness', `${percentage}%`]); + res.json(result); +}); + +// POST /monitors/:id/contrast - Set contrast +app.post('/monitors/:id/contrast', (req, res) => { + const { percentage } = req.body; + const result = runDDC(['set', req.params.id, 'contrast', `${percentage}%`]); + res.json(result); +}); + +// POST /monitors/:id/vcp - Set raw VCP value +app.post('/monitors/:id/vcp', (req, res) => { + const { code, value } = req.body; + const result = runDDC(['set', req.params.id, code, value]); + res.json(result); +}); + +// POST /monitors/:id/profile - Apply complete profile +app.post('/monitors/:id/profile', (req, res) => { + const { input, brightness, contrast } = req.body; + const results = {}; + + if (input) { + results.input = runDDC(['set', req.params.id, input]); + } + if (brightness) { + results.brightness = runDDC(['set', req.params.id, 'brightness', `${brightness}%`]); + } + if (contrast) { + results.contrast = runDDC(['set', req.params.id, 'contrast', `${contrast}%`]); + } + + res.json({ success: true, results }); +}); + app.listen(3000, () => { - console.log('DDCSwitch API running on http://localhost:3000'); + console.log('ddcswitch API running on http://localhost:3000'); + console.log('Endpoints:'); + console.log(' GET /monitors?verbose=true'); + console.log(' GET /monitors/:id/brightness'); + console.log(' POST /monitors/:id/brightness {"percentage": 75}'); + console.log(' POST /monitors/:id/profile {"input": "HDMI1", "brightness": 75, "contrast": 80}'); }); ``` @@ -321,24 +873,45 @@ app.listen(3000, () => { ```batch @echo off -REM check-and-switch.bat - Switch only if not already on target input +REM complete-setup.bat - Set input, brightness, and contrast + +echo Setting up monitor configuration... -for /f "delims=" %%i in ('DDCSwitch get 0 --json') do set JSON_OUTPUT=%%i +REM Switch to HDMI1 +for /f "delims=" %%i in ('ddcswitch set 0 HDMI1 --json') do set INPUT_RESULT=%%i +echo %INPUT_RESULT% | find "\"success\":true" >nul +if not errorlevel 1 ( + echo ? Switched to HDMI1 +) else ( + echo ? Failed to switch input +) + +REM Set brightness to 75% +for /f "delims=" %%i in ('ddcswitch set 0 brightness 75%% --json') do set BRIGHTNESS_RESULT=%%i +echo %BRIGHTNESS_RESULT% | find "\"success\":true" >nul +if not errorlevel 1 ( + echo ? Set brightness to 75%% +) else ( + echo ? Brightness not supported or failed +) -REM Simple check if contains HDMI1 -echo %JSON_OUTPUT% | find "0x11" >nul -if errorlevel 1 ( - echo Switching to HDMI1... - DDCSwitch set 0 HDMI1 +REM Set contrast to 80% +for /f "delims=" %%i in ('ddcswitch set 0 contrast 80%% --json') do set CONTRAST_RESULT=%%i +echo %CONTRAST_RESULT% | find "\"success\":true" >nul +if not errorlevel 1 ( + echo ? Set contrast to 80%% ) else ( - echo Already on HDMI1 + echo ? Contrast not supported or failed ) + +echo Monitor setup complete! +pause ``` ### Rust JSON Example ```rust -// monitor_switcher.rs +// monitor_controller.rs use serde::{Deserialize, Serialize}; use std::process::Command; @@ -354,6 +927,8 @@ struct MonitorInfo { current_input: Option, #[serde(rename = "currentInputCode")] current_input_code: Option, + brightness: Option, + contrast: Option, status: String, } @@ -364,26 +939,103 @@ struct ListResponse { error: Option, } -fn main() { +#[derive(Debug, Deserialize)] +struct SetResponse { + success: bool, + #[serde(rename = "percentageValue")] + percentage_value: Option, + #[serde(rename = "rawValue")] + raw_value: Option, + error: Option, +} + +fn run_ddc_command(args: &[&str]) -> Result> { + let mut cmd_args = args.to_vec(); + cmd_args.push("--json"); + let output = Command::new("DDCSwitch") - .args(&["list", "--json"]) - .output() - .expect("Failed to execute DDCSwitch"); + .args(&cmd_args) + .output()?; + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +fn list_monitors(verbose: bool) -> Result> { + let args = if verbose { + vec!["list", "--verbose"] + } else { + vec!["list"] + }; + + let json_str = run_ddc_command(&args)?; + let result: ListResponse = serde_json::from_str(&json_str)?; + Ok(result) +} + +fn set_brightness(monitor_index: u32, percentage: u32) -> Result> { + let args = vec!["set", &monitor_index.to_string(), "brightness", &format!("{}%", percentage)]; + let json_str = run_ddc_command(&args)?; + let result: SetResponse = serde_json::from_str(&json_str)?; + Ok(result) +} + +fn set_contrast(monitor_index: u32, percentage: u32) -> Result> { + let args = vec!["set", &monitor_index.to_string(), "contrast", &format!("{}%", percentage)]; + let json_str = run_ddc_command(&args)?; + let result: SetResponse = serde_json::from_str(&json_str)?; + Ok(result) +} - let json_str = String::from_utf8_lossy(&output.stdout); - let result: ListResponse = serde_json::from_str(&json_str) - .expect("Failed to parse JSON"); +fn main() -> Result<(), Box> { + // List monitors with verbose info + let result = list_monitors(true)?; if result.success { if let Some(monitors) = result.monitors { - for monitor in monitors { - println!("{}: {} - {:?}", + println!("Found {} monitors:", monitors.len()); + for monitor in &monitors { + println!("{}: {} - Input: {:?}", monitor.index, monitor.name, monitor.current_input); + println!(" Brightness: {:?}", monitor.brightness); + println!(" Contrast: {:?}", monitor.contrast); + } + + // Set brightness and contrast on first monitor + if !monitors.is_empty() { + let monitor_index = monitors[0].index; + + match set_brightness(monitor_index, 75) { + Ok(response) if response.success => { + println!("? Set brightness to 75%"); + } + Ok(response) => { + println!("? Failed to set brightness: {:?}", response.error); + } + Err(e) => { + println!("? Error setting brightness: {}", e); + } + } + + match set_contrast(monitor_index, 80) { + Ok(response) if response.success => { + println!("? Set contrast to 80%"); + } + Ok(response) => { + println!("? Failed to set contrast: {:?}", response.error); + } + Err(e) => { + println!("? Error setting contrast: {}", e); + } + } } } + } else { + println!("Error: {:?}", result.error); } + + Ok(()) } ``` @@ -392,38 +1044,57 @@ fn main() { ### Multi-Monitor Setup Scripts #### Scenario: Work Setup -Switch all monitors to PC inputs: +Switch all monitors to PC inputs with comfortable brightness: ```powershell # work-setup.ps1 Write-Host "Switching to work setup..." -ForegroundColor Cyan -DDCSwitch set 0 DP1 -DDCSwitch set 1 DP2 -DDCSwitch set 2 HDMI1 +ddcswitch set 0 DP1 +ddcswitch set 0 brightness 60% +ddcswitch set 0 contrast 75% +ddcswitch set 1 DP2 +ddcswitch set 1 brightness 60% +ddcswitch set 1 contrast 75% Write-Host "Work setup ready!" -ForegroundColor Green ``` #### Scenario: Gaming Setup -Switch monitors to console inputs: +Switch monitors to console inputs with high brightness: ```powershell # gaming-setup.ps1 Write-Host "Switching to gaming setup..." -ForegroundColor Cyan -DDCSwitch set 0 HDMI1 # Main monitor to PS5 -DDCSwitch set 1 HDMI2 # Secondary to Switch +ddcswitch set 0 HDMI1 # Main monitor to PS5 +ddcswitch set 0 brightness 90% +ddcswitch set 0 contrast 85% +ddcswitch set 1 HDMI2 # Secondary to Switch +ddcswitch set 1 brightness 85% +ddcswitch set 1 contrast 80% Write-Host "Gaming setup ready!" -ForegroundColor Green ``` +#### Scenario: Movie/Media Setup +Optimize for media consumption: + +```powershell +# media-setup.ps1 +Write-Host "Switching to media setup..." -ForegroundColor Cyan +ddcswitch set 0 HDMI1 # Media device +ddcswitch set 0 brightness 40% # Lower brightness for comfortable viewing +ddcswitch set 0 contrast 90% # High contrast for better blacks +Write-Host "Media setup ready!" -ForegroundColor Green +``` + ### AutoHotkey Integration -Create a comprehensive input switching system: +Create a comprehensive input switching and brightness control system: ```autohotkey -; DDCSwitch AutoHotkey Script -; Place DDCSwitch.exe in C:\Tools\ or update path below +; ddcswitch AutoHotkey Script with VCP Support +; Place ddcswitch.exe in C:\Tools\ or update path below ; Global variables -DDCSwitchPath := "C:\Tools\DDCSwitch.exe" +DDCSwitchPath := "C:\Tools\ddcswitch.exe" ; Function to run DDCSwitch RunDDCSwitch(args) { @@ -431,66 +1102,152 @@ RunDDCSwitch(args) { Run, %DDCSwitchPath% %args%, , Hide } +; Input switching hotkeys ; Ctrl+Alt+1: Switch monitor 0 to HDMI1 ^!1:: RunDDCSwitch("set 0 HDMI1") - TrayTip, DDCSwitch, Switched to HDMI1, 1 + TrayTip, ddcswitch, Switched to HDMI1, 1 return ; Ctrl+Alt+2: Switch monitor 0 to HDMI2 ^!2:: RunDDCSwitch("set 0 HDMI2") - TrayTip, DDCSwitch, Switched to HDMI2, 1 + TrayTip, ddcswitch, Switched to HDMI2, 1 return ; Ctrl+Alt+D: Switch monitor 0 to DisplayPort ^!d:: RunDDCSwitch("set 0 DP1") - TrayTip, DDCSwitch, Switched to DisplayPort, 1 + TrayTip, ddcswitch, Switched to DisplayPort, 1 + return + +; Brightness control hotkeys +; Ctrl+Alt+Plus: Increase brightness by 10% +^!NumpadAdd:: +^!=:: + RunDDCSwitch("set 0 brightness +10%") + TrayTip, ddcswitch, Brightness increased, 1 + return + +; Ctrl+Alt+Minus: Decrease brightness by 10% +^!NumpadSub:: +^!-:: + RunDDCSwitch("set 0 brightness -10%") + TrayTip, ddcswitch, Brightness decreased, 1 + return + +; Brightness presets +; Ctrl+Alt+F1: 25% brightness (night mode) +^!F1:: + RunDDCSwitch("set 0 brightness 25%") + TrayTip, ddcswitch, Night Mode (25%), 1 + return + +; Ctrl+Alt+F2: 50% brightness (comfortable) +^!F2:: + RunDDCSwitch("set 0 brightness 50%") + TrayTip, ddcswitch, Comfortable (50%), 1 + return + +; Ctrl+Alt+F3: 75% brightness (bright) +^!F3:: + RunDDCSwitch("set 0 brightness 75%") + TrayTip, ddcswitch, Bright (75%), 1 + return + +; Ctrl+Alt+F4: 100% brightness (maximum) +^!F4:: + RunDDCSwitch("set 0 brightness 100%") + TrayTip, ddcswitch, Maximum (100%), 1 return -; Ctrl+Alt+W: Work setup (all monitors to PC) +; Profile hotkeys +; Ctrl+Alt+W: Work setup (all monitors to PC with comfortable settings) ^!w:: RunDDCSwitch("set 0 DP1") Sleep 500 + RunDDCSwitch("set 0 brightness 60%") + Sleep 500 + RunDDCSwitch("set 0 contrast 75%") + Sleep 500 RunDDCSwitch("set 1 DP2") - TrayTip, DDCSwitch, Work Setup Activated, 1 + TrayTip, ddcswitch, Work Setup Activated, 1 return -; Ctrl+Alt+G: Gaming setup (all monitors to console) +; Ctrl+Alt+G: Gaming setup (all monitors to console with high brightness) ^!g:: RunDDCSwitch("set 0 HDMI1") Sleep 500 + RunDDCSwitch("set 0 brightness 90%") + Sleep 500 + RunDDCSwitch("set 0 contrast 85%") + Sleep 500 RunDDCSwitch("set 1 HDMI2") - TrayTip, DDCSwitch, Gaming Setup Activated, 1 + TrayTip, ddcswitch, Gaming Setup Activated, 1 + return + +; Ctrl+Alt+M: Media setup (HDMI with low brightness, high contrast) +^!m:: + RunDDCSwitch("set 0 HDMI1") + Sleep 500 + RunDDCSwitch("set 0 brightness 40%") + Sleep 500 + RunDDCSwitch("set 0 contrast 90%") + TrayTip, ddcswitch, Media Setup Activated, 1 return -; Ctrl+Alt+L: List all monitors +; Ctrl+Alt+L: List all monitors with verbose info ^!l:: - Run, cmd /k DDCSwitch.exe list + Run, cmd /k ddcswitch.exe list --verbose + return + +; Ctrl+Alt+I: Show current monitor info +^!i:: + Run, cmd /k "ddcswitch.exe get 0 && ddcswitch.exe get 0 brightness && ddcswitch.exe get 0 contrast && pause" return ``` ### Stream Deck Integration -If you use Elgato Stream Deck, create a "System" action with these commands: +If you use Elgato Stream Deck, create actions for complete monitor control: -**Button 1: PC Input** +**Button 1: PC Mode** ``` Title: PC Mode -Command: C:\Tools\DDCSwitch.exe set 0 DP1 +Command: C:\Tools\ddcswitch.exe set 0 DP1 +Arguments: && C:\Tools\ddcswitch.exe set 0 brightness 60% ``` -**Button 2: Console Input** +**Button 2: Console Mode** ``` Title: Console Mode -Command: C:\Tools\DDCSwitch.exe set 0 HDMI1 +Command: C:\Tools\ddcswitch.exe set 0 HDMI1 +Arguments: && C:\Tools\ddcswitch.exe set 0 brightness 90% ``` -**Button 3: List Monitors** +**Button 3: Brightness Low** +``` +Title: ?? Low +Command: C:\Tools\ddcswitch.exe set 0 brightness 25% +``` + +**Button 4: Brightness High** +``` +Title: ?? High +Command: C:\Tools\ddcswitch.exe set 0 brightness 85% +``` + +**Button 5: Monitor Info** ``` Title: Monitor Info -Command: cmd /k C:\Tools\DDCSwitch.exe list +Command: cmd /k C:\Tools\ddcswitch.exe list --verbose +``` + +**Button 6: Gaming Profile** +``` +Title: ?? Gaming +Command: C:\Tools\ddcswitch.exe set 0 HDMI1 +Arguments: && timeout /t 1 && C:\Tools\ddcswitch.exe set 0 brightness 90% && C:\Tools\ddcswitch.exe set 0 contrast 85% ``` ### Task Scheduler Integration @@ -500,10 +1257,10 @@ Automatically switch inputs at specific times: #### Morning: Switch to Work Setup (8 AM) 1. Open Task Scheduler -2. Create Basic Task → "Morning Work Setup" +2. Create Basic Task ? "Morning Work Setup" 3. Trigger: Daily at 8:00 AM 4. Action: Start a program - - Program: `C:\Tools\DDCSwitch.exe` + - Program: `C:\Tools\ddcswitch.exe` - Arguments: `set 0 DP1` #### Evening: Switch to Gaming Setup (6 PM) @@ -515,57 +1272,218 @@ Same steps, but with trigger at 6:00 PM and arguments: `set 0 HDMI1` Add to your PowerShell profile (`$PROFILE`): ```powershell -# DDCSwitch aliases -function ddc-list { DDCSwitch list } +# ddcswitch aliases for complete monitor control +function ddc-list { ddcswitch list --verbose } function ddc-work { - DDCSwitch set 0 DP1 - DDCSwitch set 1 DP2 - Write-Host "✓ Work setup activated" -ForegroundColor Green + ddcswitch set 0 DP1 + ddcswitch set 0 brightness 60% + ddcswitch set 0 contrast 75% + ddcswitch set 1 DP2 + Write-Host "? Work setup activated" -ForegroundColor Green } function ddc-game { - DDCSwitch set 0 HDMI1 - DDCSwitch set 1 HDMI2 - Write-Host "✓ Gaming setup activated" -ForegroundColor Green + ddcswitch set 0 HDMI1 + ddcswitch set 0 brightness 90% + ddcswitch set 0 contrast 85% + ddcswitch set 1 HDMI2 + Write-Host "? Gaming setup activated" -ForegroundColor Green +} +function ddc-media { + ddcswitch set 0 HDMI1 + ddcswitch set 0 brightness 40% + ddcswitch set 0 contrast 90% + Write-Host "? Media setup activated" -ForegroundColor Green +} +function ddc-hdmi { ddcswitch set 0 HDMI1 } +function ddc-dp { ddcswitch set 0 DP1 } +function ddc-bright([int]$level) { ddcswitch set 0 brightness "$level%" } +function ddc-contrast([int]$level) { ddcswitch set 0 contrast "$level%" } + +# Brightness shortcuts +function ddc-dim { ddcswitch set 0 brightness 25% } +function ddc-normal { ddcswitch set 0 brightness 60% } +function ddc-bright { ddcswitch set 0 brightness 85% } +function ddc-max { ddcswitch set 0 brightness 100% } + +# Then use: ddc-work, ddc-game, ddc-media, ddc-bright 75, ddc-list +``` + +### Complete Monitor Control + +Use ddcswitch as a comprehensive monitor management solution: + +```powershell +# complete-monitor-control.ps1 +param( + [string]$Profile = "work", # work, gaming, media, custom + [int]$Monitor = 0, + [string]$Input, + [int]$Brightness, + [int]$Contrast +) + +function Apply-Profile { + param($ProfileName, $MonitorIndex) + + switch ($ProfileName.ToLower()) { + "work" { + ddcswitch set $MonitorIndex DP1 + ddcswitch set $MonitorIndex brightness 60% + ddcswitch set $MonitorIndex contrast 75% + Write-Host "? Applied work profile" -ForegroundColor Green + } + "gaming" { + ddcswitch set $MonitorIndex HDMI1 + ddcswitch set $MonitorIndex brightness 90% + ddcswitch set $MonitorIndex contrast 85% + Write-Host "? Applied gaming profile" -ForegroundColor Green + } + "media" { + ddcswitch set $MonitorIndex HDMI1 + ddcswitch set $MonitorIndex brightness 40% + ddcswitch set $MonitorIndex contrast 90% + Write-Host "? Applied media profile" -ForegroundColor Green + } + "custom" { + if ($Input) { ddcswitch set $MonitorIndex $Input } + if ($Brightness) { ddcswitch set $MonitorIndex brightness "$Brightness%" } + if ($Contrast) { ddcswitch set $MonitorIndex contrast "$Contrast%" } + Write-Host "? Applied custom settings" -ForegroundColor Green + } + } } -function ddc-hdmi { DDCSwitch set 0 HDMI1 } -function ddc-dp { DDCSwitch set 0 DP1 } -# Then use: ddc-work, ddc-game, ddc-hdmi, ddc-dp, ddc-list +Apply-Profile -ProfileName $Profile -MonitorIndex $Monitor + +# Usage examples: +# .\complete-monitor-control.ps1 -Profile work +# .\complete-monitor-control.ps1 -Profile gaming -Monitor 1 +# .\complete-monitor-control.ps1 -Profile custom -Input HDMI2 -Brightness 75 -Contrast 80 ``` -### KVM Switch Replacement +### Testing Monitor VCP Support -Use DDCSwitch as a software KVM (without USB switching): +Test what VCP features your monitor supports: ```powershell -# kvm-to-pc1.ps1 -DDCSwitch set 0 DP1 -DDCSwitch set 1 DP1 -Write-Host "Switched all monitors to PC1" -ForegroundColor Green +# test-vcp-support.ps1 - Test brightness, contrast, and other VCP features +$monitor = 0 + +Write-Host "Testing VCP feature support for Monitor $monitor" -ForegroundColor Cyan +Write-Host "=" * 50 + +# Test brightness support +Write-Host "`nTesting Brightness (VCP 0x10)..." -ForegroundColor Yellow +try { + $brightness = ddcswitch get $monitor brightness 2>$null + if ($brightness -match "Brightness:") { + Write-Host "? Brightness supported: $brightness" -ForegroundColor Green + + # Test setting brightness + ddcswitch set $monitor brightness 50% | Out-Null + Start-Sleep -Seconds 1 + $newBrightness = ddcswitch get $monitor brightness + Write-Host "? Brightness control works: $newBrightness" -ForegroundColor Green + } else { + Write-Host "? Brightness not supported" -ForegroundColor Red + } +} catch { + Write-Host "? Brightness not supported" -ForegroundColor Red +} + +# Test contrast support +Write-Host "`nTesting Contrast (VCP 0x12)..." -ForegroundColor Yellow +try { + $contrast = ddcswitch get $monitor contrast 2>$null + if ($contrast -match "Contrast:") { + Write-Host "? Contrast supported: $contrast" -ForegroundColor Green + + # Test setting contrast + ddcswitch set $monitor contrast 75% | Out-Null + Start-Sleep -Seconds 1 + $newContrast = ddcswitch get $monitor contrast + Write-Host "? Contrast control works: $newContrast" -ForegroundColor Green + } else { + Write-Host "? Contrast not supported" -ForegroundColor Red + } +} catch { + Write-Host "? Contrast not supported" -ForegroundColor Red +} + +# Test raw VCP codes +Write-Host "`nTesting Raw VCP Codes..." -ForegroundColor Yellow +$vcpCodes = @{ + "0x10" = "Brightness" + "0x12" = "Contrast" + "0x14" = "Color Temperature" + "0x16" = "Red Gain" + "0x18" = "Green Gain" + "0x1A" = "Blue Gain" + "0x60" = "Input Source" + "0x62" = "Audio Volume" + "0x6C" = "Red Black Level" + "0x6E" = "Green Black Level" + "0x70" = "Blue Black Level" +} + +foreach ($code in $vcpCodes.Keys) { + try { + $result = ddcswitch get $monitor $code 2>$null + if ($result -and $result -notmatch "error|failed|not supported") { + Write-Host "? VCP $code ($($vcpCodes[$code])): $result" -ForegroundColor Green + } else { + Write-Host "? VCP $code ($($vcpCodes[$code])): Not supported" -ForegroundColor Gray + } + } catch { + Write-Host "? VCP $code ($($vcpCodes[$code])): Not supported" -ForegroundColor Gray + } +} -# kvm-to-pc2.ps1 -DDCSwitch set 0 DP2 -DDCSwitch set 1 DP2 -Write-Host "Switched all monitors to PC2" -ForegroundColor Green +Write-Host "`nVCP Support Test Complete!" -ForegroundColor Cyan ``` -### Testing Monitor Compatibility +### Finding Optimal Settings -If your monitor doesn't respond to standard codes, try discovering the correct codes: +Find the best brightness and contrast settings for different scenarios: ```powershell -# Test different input codes +# find-optimal-settings.ps1 +Write-Host "Monitor Calibration Helper" -ForegroundColor Cyan +Write-Host "This script will cycle through different brightness/contrast combinations" +Write-Host "Press any key after each setting to continue, or Ctrl+C to stop" +Write-Host "" + $monitor = 0 -foreach ($code in 0x01, 0x03, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15) { - Write-Host "Testing code 0x$($code.ToString('X2'))..." - DDCSwitch set $monitor "0x$($code.ToString('X2'))" - Start-Sleep -Seconds 3 +$brightnessLevels = @(25, 40, 50, 60, 75, 85, 100) +$contrastLevels = @(60, 70, 75, 80, 85, 90, 95) + +foreach ($brightness in $brightnessLevels) { + foreach ($contrast in $contrastLevels) { + Write-Host "Setting: Brightness $brightness%, Contrast $contrast%" -ForegroundColor Yellow + + ddcswitch set $monitor brightness "$brightness%" | Out-Null + Start-Sleep -Milliseconds 500 + ddcswitch set $monitor contrast "$contrast%" | Out-Null + + Write-Host "How does this look? (Press Enter to continue, 'q' to quit, 's' to save this setting)" -ForegroundColor Green + $input = Read-Host + + if ($input -eq 'q') { + Write-Host "Calibration stopped." -ForegroundColor Red + break + } elseif ($input -eq 's') { + Write-Host "Saved setting: Brightness $brightness%, Contrast $contrast%" -ForegroundColor Cyan + Write-Host "Command to reproduce: ddcswitch set $monitor brightness $brightness%; ddcswitch set $monitor contrast $contrast%" -ForegroundColor White + Read-Host "Press Enter to continue or Ctrl+C to stop" + } + } + if ($input -eq 'q') { break } } ``` ### Finding Non-Standard VCP Codes -Some monitors use non-standard DDC/CI codes. If DDCSwitch shows the wrong current input or switching doesn't work, you can find the correct codes: +Some monitors use non-standard DDC/CI codes. If ddcswitch shows the wrong current input or switching doesn't work, you can find the correct codes: #### Method 1: Use ControlMyMonitor by NirSoft @@ -581,15 +1499,15 @@ Some monitors use non-standard DDC/CI codes. If DDCSwitch shows the wrong curren **Example:** ``` VCP Code 60 (Input Source): -- HDMI1 physical input → Shows value 17 (0x11) ✓ Standard -- HDMI2 physical input → Shows value 18 (0x12) ✓ Standard -- DisplayPort physical input → Shows value 15 (0x0F) ✓ Standard -- DisplayPort physical input → Shows value 27 (0x1B) ✗ Non-standard! +- HDMI1 physical input ? Shows value 17 (0x11) ? Standard +- HDMI2 physical input ? Shows value 18 (0x12) ? Standard +- DisplayPort physical input ? Shows value 15 (0x0F) ? Standard +- DisplayPort physical input ? Shows value 27 (0x1B) ? Non-standard! ``` Once you know the correct codes, use them with DDCSwitch: ```powershell -DDCSwitch set 0 0x1B # Use the actual code your monitor responds to +ddcswitch set 0 0x1B # Use the actual code your monitor responds to ``` #### Method 2: Trial and Error @@ -607,7 +1525,7 @@ $codes = @(0x01, 0x02, 0x03, 0x04, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x1 foreach ($code in $codes) { $hexCode = "0x{0:X2}" -f $code Write-Host "Testing $hexCode..." -ForegroundColor Green - DDCSwitch set 0 $hexCode + ddcswitch set 0 $hexCode Start-Sleep -Seconds 3 } @@ -621,11 +1539,17 @@ Write-Host "Testing complete! Document which codes worked for your monitor." -Fo ```powershell # Get current input (if this works, DDC/CI is functional) -DDCSwitch get 0 +ddcswitch get 0 + +# Get by monitor name +ddcswitch get "VG270U" + +# List all monitors with all VCP values +ddcswitch get all # Try setting to current input (should succeed instantly) -DDCSwitch list # Note the current input -DDCSwitch set 0 +ddcswitch list # Note the current input +ddcswitch set 0 ``` ### Dealing with Slow Monitors @@ -634,9 +1558,9 @@ Some monitors are slow to respond to DDC/CI commands. Add delays: ```powershell # slow-switch.ps1 -DDCSwitch set 0 HDMI1 +ddcswitch set 0 HDMI1 Start-Sleep -Seconds 2 # Wait for monitor to switch -DDCSwitch set 1 HDMI2 +ddcswitch set 1 HDMI2 Start-Sleep -Seconds 2 Write-Host "Done" -ForegroundColor Green ``` @@ -647,21 +1571,25 @@ Useful if monitor order changes: ```powershell # Find monitor with "LG" in the name and switch it -DDCSwitch list # Note the exact name -DDCSwitch set "LG ULTRAGEAR" HDMI1 +ddcswitch list # Note the exact name +ddcswitch set "LG ULTRAGEAR" HDMI1 # Partial name matching works -DDCSwitch set "ULTRAGEAR" HDMI1 +ddcswitch set "ULTRAGEAR" HDMI1 + +# Get settings by monitor name +ddcswitch get "VG270U" brightness +ddcswitch get "Generic PnP" # Gets all VCP values for this monitor ``` ## Integration with Other Tools ### PowerToys Run -Add DDCSwitch to your PATH, then use PowerToys Run (Alt+Space): +Add ddcswitch to your PATH, then use PowerToys Run (Alt+Space): ``` -> DDCSwitch set 0 HDMI1 +> ddcswitch set 0 HDMI1 ``` ### Windows Run Dialog @@ -669,7 +1597,7 @@ Add DDCSwitch to your PATH, then use PowerToys Run (Alt+Space): Press Win+R and type: ``` -DDCSwitch set 0 DP1 +ddcswitch set 0 DP1 ``` ### Batch File Shortcuts @@ -679,66 +1607,106 @@ Create `.bat` files on your desktop: **switch-to-hdmi.bat:** ```batch @echo off -"C:\Tools\DDCSwitch.exe" set 0 HDMI1 +"C:\Tools\ddcswitch.exe" set 0 HDMI1 ``` **switch-to-dp.bat:** ```batch @echo off -"C:\Tools\DDCSwitch.exe" set 0 DP1 +"C:\Tools\ddcswitch.exe" set 0 DP1 ``` Make them double-clickable for quick access! ## Tips and Tricks -1. **Add to PATH**: Add DDCSwitch.exe location to your Windows PATH for easier access -2. **Create shortcuts**: Right-click DDCSwitch.exe → Send to → Desktop (create shortcut), then edit properties to add arguments +1. **Add to PATH**: Add ddcswitch.exe location to your Windows PATH for easier access +2. **Create shortcuts**: Right-click ddcswitch.exe ? Send to ? Desktop (create shortcut), then edit properties to add arguments 3. **Use monitoring**: Combine with other tools to detect when certain apps launch and switch inputs automatically 4. **Test first**: Always test with `list` and `get` before creating automation scripts 5. **Admin rights**: Some monitors require running as Administrator - right-click and "Run as administrator" ## Common Patterns -### Pattern 1: Toggle Between Two Inputs +### Pattern 1: Toggle Between Settings ```powershell -# toggle-input.ps1 -$current = (DDCSwitch get 0 | Select-String -Pattern "0x([0-9A-F]{2})").Matches[0].Groups[1].Value -$currentDecimal = [Convert]::ToInt32($current, 16) +# toggle-brightness.ps1 - Toggle between low/medium/high brightness +$current = ddcswitch get 0 brightness --json | ConvertFrom-Json -if ($currentDecimal -eq 0x11) { # Currently HDMI1 - DDCSwitch set 0 DP1 - Write-Host "Switched to DisplayPort" +if ($current.success) { + $currentPercent = $current.percentageValue + + # Cycle through 25% ? 50% ? 75% ? 100% ? 25% + $newBrightness = switch ($currentPercent) { + {$_ -le 25} { 50 } + {$_ -le 50} { 75 } + {$_ -le 75} { 100 } + default { 25 } + } + + ddcswitch set 0 brightness "$newBrightness%" + Write-Host "Brightness: $currentPercent% ? $newBrightness%" -ForegroundColor Green } else { - DDCSwitch set 0 HDMI1 - Write-Host "Switched to HDMI1" + Write-Host "Brightness control not supported" -ForegroundColor Red } ``` -### Pattern 2: Check Before Switch +### Pattern 2: Check Before Set ```powershell -# smart-switch.ps1 -param([string]$Input = "HDMI1") +# smart-profile-switch.ps1 +param([string]$Profile = "work") + +# Get current settings +$inputResult = ddcswitch get 0 --json | ConvertFrom-Json +$brightnessResult = ddcswitch get 0 brightness --json | ConvertFrom-Json + +if (-not $inputResult.success) { + Write-Error "Monitor not accessible" + exit 1 +} -$monitors = DDCSwitch list -if ($monitors -match "No DDC/CI") { - Write-Error "No compatible monitors found" +# Define profiles +$profiles = @{ + "work" = @{ input = "DP1"; brightness = 60; contrast = 75 } + "gaming" = @{ input = "HDMI1"; brightness = 90; contrast = 85 } + "media" = @{ input = "HDMI1"; brightness = 40; contrast = 90 } +} + +if (-not $profiles.ContainsKey($Profile)) { + Write-Error "Unknown profile: $Profile. Available: work, gaming, media" exit 1 } -DDCSwitch set 0 $Input -Write-Host "Successfully switched to $Input" -ForegroundColor Green +$targetProfile = $profiles[$Profile] + +# Apply settings only if different +if ($inputResult.currentInputCode -ne $targetProfile.input) { + ddcswitch set 0 $targetProfile.input + Write-Host "? Input: $($targetProfile.input)" -ForegroundColor Green +} + +if ($brightnessResult.success -and $brightnessResult.percentageValue -ne $targetProfile.brightness) { + ddcswitch set 0 brightness "$($targetProfile.brightness)%" + Write-Host "? Brightness: $($targetProfile.brightness)%" -ForegroundColor Green +} + +ddcswitch set 0 contrast "$($targetProfile.contrast)%" +Write-Host "? Profile '$Profile' applied" -ForegroundColor Cyan ``` -### Pattern 3: Switch All Monitors to Same Input +### Pattern 3: Sync All Monitors ```powershell -# sync-all.ps1 - Using JSON for reliable monitor enumeration -param([string]$Input = "HDMI1") +# sync-all-monitors.ps1 - Apply same settings to all monitors +param( + [string]$Input = "HDMI1", + [int]$Brightness = 75, + [int]$Contrast = 80 +) -$result = DDCSwitch list --json | ConvertFrom-Json +$result = ddcswitch list --json | ConvertFrom-Json if (-not $result.success) { Write-Error $result.error @@ -748,30 +1716,37 @@ if (-not $result.success) { $okMonitors = $result.monitors | Where-Object { $_.status -eq "ok" } foreach ($monitor in $okMonitors) { - Write-Host "Switching monitor $($monitor.index) ($($monitor.name)) to $Input..." - DDCSwitch set $monitor.index $Input - Start-Sleep -Milliseconds 500 -} - -Write-Host "Switched $($okMonitors.Count) monitors to $Input" -ForegroundColor Green -``` - -**Alternative (without JSON):** -```powershell -# sync-all-legacy.ps1 -param([string]$Input = "HDMI1") - -$output = DDCSwitch list -$monitorCount = ($output | Select-String -Pattern "^\│ \d+" -AllMatches).Matches.Count - -for ($i = 0; $i -lt $monitorCount; $i++) { - Write-Host "Switching monitor $i to $Input..." - DDCSwitch set $i $Input - Start-Sleep -Milliseconds 500 + Write-Host "Configuring monitor $($monitor.index) ($($monitor.name))..." -ForegroundColor Cyan + + # Set input + $inputResult = ddcswitch set $monitor.index $Input --json | ConvertFrom-Json + if ($inputResult.success) { + Write-Host " ? Input: $Input" -ForegroundColor Green + } else { + Write-Host " ? Input failed: $($inputResult.error)" -ForegroundColor Red + } + + # Set brightness + $brightnessResult = ddcswitch set $monitor.index brightness "$Brightness%" --json | ConvertFrom-Json + if ($brightnessResult.success) { + Write-Host " ? Brightness: $Brightness%" -ForegroundColor Green + } else { + Write-Host " ? Brightness not supported" -ForegroundColor Yellow + } + + # Set contrast + $contrastResult = ddcswitch set $monitor.index contrast "$Contrast%" --json | ConvertFrom-Json + if ($contrastResult.success) { + Write-Host " ? Contrast: $Contrast%" -ForegroundColor Green + } else { + Write-Host " ? Contrast not supported" -ForegroundColor Yellow + } + + Start-Sleep -Milliseconds 500 # Prevent DDC/CI overload } -Write-Host "All monitors switched to $Input" -ForegroundColor Green +Write-Host "Synchronized $($okMonitors.Count) monitors" -ForegroundColor Cyan ``` -Happy switching! +Happy switching and brightness controlling! diff --git a/README.md b/README.md index 5b00607..a6cc656 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DDCSwitch +# ddcswitch [![GitHub Release](https://img.shields.io/github/v/release/markdwags/DDCSwitch)](https://github.com/markdwags/DDCSwitch/releases) [![License](https://img.shields.io/github/license/markdwags/DDCSwitch)](https://github.com/markdwags/DDCSwitch/blob/main/LICENSE) @@ -6,7 +6,7 @@ [![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![JSON Output](https://img.shields.io/badge/JSON-Output%20Support-green)](https://github.com/markdwags/DDCSwitch#json-output-for-automation) -A Windows command-line utility to control monitor input sources via DDC/CI (Display Data Channel Command Interface). Switch between HDMI, DisplayPort, DVI, and VGA inputs without touching physical buttons. +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. 📚 **[Examples](EXAMPLES.md)** | 📝 **[Changelog](CHANGELOG.md)** @@ -14,6 +14,10 @@ A Windows command-line utility to control monitor input sources via DDC/CI (Disp - 🖥️ **List all DDC/CI capable monitors** with their current input sources - 🔄 **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 @@ -24,7 +28,7 @@ A Windows command-line utility to control monitor input sources via DDC/CI (Disp ### 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. ### Build from Source @@ -43,7 +47,7 @@ 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` +Executable location: `DDCSwitch/bin/Release/net10.0/win-x64/publish/ddcswitch.exe` ## Usage @@ -52,7 +56,7 @@ Executable location: `DDCSwitch/bin/Release/net10.0/win-x64/publish/DDCSwitch.ex Display all DDC/CI capable monitors with their current input sources: ```powershell -DDCSwitch list +ddcswitch list ``` Example output: @@ -65,34 +69,150 @@ Example output: ╰───────┴─────────────────────┴──────────────┴───────────────────────────┴────────╯ ``` +#### 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). -### Get Current Input +### 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): + +```powershell +# Get all settings by monitor name +ddcswitch get "VG270U P" +ddcswitch get "Generic PnP" +``` -Get the current input source for a specific monitor: +Get a specific feature: ```powershell -DDCSwitch get 0 +# Get current input source +ddcswitch get 0 input + +# Get brightness as percentage +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 ``` -Output: `Monitor: Generic PnP Monitor (\\.\DISPLAY2)` / `Current Input: HDMI1 (0x11)` +Output: `Monitor: Generic PnP Monitor` / `Brightness: 75% (120/160)` -### Set Input Source +### Set Monitor Settings Switch a monitor to a different input: ```powershell # By monitor index -DDCSwitch set 0 HDMI1 +ddcswitch set 0 HDMI1 # By monitor name (partial match) -DDCSwitch set "LG ULTRAGEAR" HDMI2 +ddcswitch set "LG ULTRAGEAR" HDMI2 +``` + +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: + +```powershell +# Get raw VCP value (e.g., VCP code 0x10 for brightness) +ddcswitch get 0 0x10 + +# Set raw VCP value +ddcswitch set 0 0x10 120 +``` + +### VCP Feature Scanning + +Discover all supported VCP features on all monitors: + +```powershell +ddcswitch get all +``` + +This scans all VCP codes (0x00-0xFF) for every monitor and displays supported features with their current values, maximum values, and access types (read-only, write-only, read-write). + +To scan a specific monitor: + +```powershell +# Scan specific monitor by index +ddcswitch get 0 + +# Scan specific monitor by name +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 ``` -Output: `✓ Successfully switched Generic PnP Monitor to HDMI1` +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 Input Names +### Supported Features +#### Input Sources - **HDMI**: `HDMI1`, `HDMI2` - **DisplayPort**: `DP1`, `DP2`, `DisplayPort1`, `DisplayPort2` - **DVI**: `DVI1`, `DVI2` @@ -100,23 +220,77 @@ Output: `✓ Successfully switched Generic PnP Monitor to HDMI1` - **Other**: `SVideo1`, `SVideo2`, `Tuner1`, `ComponentVideo1`, etc. - **Custom codes**: Use hex values like `0x11` for manufacturer-specific inputs +#### Common VCP Features +- **Brightness**: `brightness` (VCP 0x10) - accepts percentage values (0-100%) +- **Contrast**: `contrast` (VCP 0x12) - accepts percentage values (0-100%) +- **Input Source**: `input` (VCP 0x60) - existing functionality maintained +- **Color Controls**: `red-gain`, `green-gain`, `blue-gain` (VCP 0x16, 0x18, 0x1A) +- **Audio**: `volume`, `mute` (VCP 0x62, 0x8D) - volume accepts percentage values +- **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 Examples **Switch multiple monitors:** ```powershell -DDCSwitch set 0 HDMI1 -DDCSwitch set 1 DP1 +ddcswitch set 0 HDMI1 +ddcswitch set 1 DP1 +``` + +**Control comprehensive VCP features:** +```powershell +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 + +# Search for features by name +ddcswitch get 0 bright # Matches "brightness" + +# Or by monitor name +ddcswitch get "VG270U" bright ``` **Desktop shortcut:** -Create a shortcut with target: `C:\Path\To\DDCSwitch.exe set 0 HDMI1` +Create a shortcut with target: `C:\Path\To\ddcswitch.exe set 0 brightness 50%` **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 +^!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 ``` ### JSON Output for Automation @@ -125,20 +299,20 @@ All commands support `--json` for machine-readable output: ```powershell # PowerShell: Conditional switching -$result = DDCSwitch get 0 --json | ConvertFrom-Json +$result = ddcswitch get 0 --json | ConvertFrom-Json if ($result.currentInputCode -ne "0x11") { - DDCSwitch set 0 HDMI1 + ddcswitch set 0 HDMI1 } ``` ```python # Python: Switch all monitors import subprocess, json -data = json.loads(subprocess.run(['DDCSwitch', 'list', '--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']) + subprocess.run(['ddcswitch', 'set', str(m['index']), 'HDMI1']) ``` 📚 **See [EXAMPLES.md](EXAMPLES.md) for comprehensive automation examples** including Stream Deck, Task Scheduler, Python, Node.js, Rust, and more. @@ -171,11 +345,17 @@ If you need to verify DDC/CI values or troubleshoot monitor-specific issues, try ## Technical Details -DDCSwitch uses the Windows DXVA2 API to communicate with monitors via DDC/CI protocol. It reads/writes VCP (Virtual Control Panel) feature 0x60 (Input Source) following the MCCS specification. +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 Input Codes:** +**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. ## Why Windows Only? @@ -192,6 +372,6 @@ MIT License - see LICENSE file for details ## Acknowledgments -- Inspired by `ddcutil` for Linux +- Inspired by [ddcutil](https://www.ddcutil.com) for Linux - Uses Spectre.Console for beautiful terminal output diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..2b507cc --- /dev/null +++ b/build.cmd @@ -0,0 +1,53 @@ +@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/chocolatey/ddcswitch.nuspec b/chocolatey/ddcswitch.nuspec new file mode 100644 index 0000000..83e762a --- /dev/null +++ b/chocolatey/ddcswitch.nuspec @@ -0,0 +1,48 @@ + + + + ddcswitch + __VERSION__ + https://github.com/markdwags/DDCSwitch + markdwags + DDCSwitch + markdwags + https://github.com/markdwags/DDCSwitch + https://cdn.jsdelivr.net/gh/markdwags/DDCSwitch@main/icon.png + https://github.com/markdwags/DDCSwitch/blob/main/LICENSE + false + https://github.com/markdwags/DDCSwitch + https://github.com/markdwags/DDCSwitch#readme + https://github.com/markdwags/DDCSwitch/issues + ddcswitch ddc-ci monitor display cli input-switching brightness contrast vcp-features admin + Control monitor settings via DDC/CI from the command line + + https://github.com/markdwags/DDCSwitch/blob/main/CHANGELOG.md + + + + + diff --git a/chocolatey/tools/CHECKSUM b/chocolatey/tools/CHECKSUM new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/chocolatey/tools/CHECKSUM @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/chocolatey/tools/VERIFICATION.txt b/chocolatey/tools/VERIFICATION.txt new file mode 100644 index 0000000..c7fd512 --- /dev/null +++ b/chocolatey/tools/VERIFICATION.txt @@ -0,0 +1,22 @@ +VERIFICATION + +Verification is intended to assist the Chocolatey moderators and community +in verifying that this package's contents are trustworthy. + +Package can be verified like this: + +1. Download the following: + + x64: https://github.com/markdwags/DDCSwitch/releases/download/v__VERSION__/ddcswitch-__VERSION__-win-x64.zip + +2. You can use one of the following methods to obtain the SHA256 checksum: + - Use powershell function 'Get-FileHash' + - Use Chocolatey utility 'checksum.exe' + + checksum64: __CHECKSUM__ + +The binary is downloaded directly from the official GitHub releases. + +Project source: https://github.com/markdwags/ddcswitch +Releases: https://github.com/markdwags/ddcswitch/releases + diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1 new file mode 100644 index 0000000..4ed9379 --- /dev/null +++ b/chocolatey/tools/chocolateyinstall.ps1 @@ -0,0 +1,28 @@ +$ErrorActionPreference = 'Stop' + +$packageName = 'ddcswitch' +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" + +# Version is automatically provided by Chocolatey from the nuspec +$version = $env:chocolateyPackageVersion +$url64 = "https://github.com/markdwags/ddcswitch/releases/download/v$version/ddcswitch-$version-win-x64.zip" + +# Checksum is stored in a separate file created during package build +$checksumFile = Join-Path $toolsDir "CHECKSUM" +$checksum64 = Get-Content $checksumFile -Raw +$checksum64 = $checksum64.Trim() +$checksumType64 = 'sha256' + +$packageArgs = @{ + packageName = $packageName + unzipLocation = $toolsDir + url64bit = $url64 + checksum64 = $checksum64 + checksumType64 = $checksumType64 +} + +Install-ChocolateyZipPackage @packageArgs + +# The ZIP contains the executable at the root +# Chocolatey will automatically create a shim for ddcswitch.exe + diff --git a/chocolatey/tools/chocolateyuninstall.ps1 b/chocolatey/tools/chocolateyuninstall.ps1 new file mode 100644 index 0000000..9abf76a --- /dev/null +++ b/chocolatey/tools/chocolateyuninstall.ps1 @@ -0,0 +1,10 @@ +$ErrorActionPreference = 'Stop' + +$packageName = 'ddcswitch' +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" + +# Remove the executable (Chocolatey handles shim removal automatically) +Remove-Item "$toolsDir\ddcswitch.exe" -ErrorAction SilentlyContinue -Force + +Write-Host "$packageName has been uninstalled successfully." + diff --git a/tools/GenerateVcpFeatures.py b/tools/GenerateVcpFeatures.py new file mode 100644 index 0000000..f2c5355 --- /dev/null +++ b/tools/GenerateVcpFeatures.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Generates VcpFeature static properties from VcpFeatureData.json. +This creates a partial class that can be combined with the core VcpFeature class. +""" + +import json +from pathlib import Path + +def generate_feature_property(feature): + """Generate a C# static property for a VCP feature.""" + code = feature['code'] + prop_name = feature['property'] + name = feature['name'] + description = feature['description'] + feature_type = feature['type'] + category = feature['category'] + supports_percentage = 'true' if feature['supportsPercentage'] else 'false' + aliases = feature.get('aliases', []) + + # Generate XML doc comment + lines = [ + f" /// ", + f" /// {description} (VCP {code})", + f" /// " + ] + + # Generate property + if aliases: + aliases_str = ', '.join(f'"{alias}"' for alias in aliases) + lines.append(f' public static VcpFeature {prop_name} => new({code}, "{name}", "{description}", VcpFeatureType.{feature_type}, VcpFeatureCategory.{category}, {supports_percentage}, {aliases_str});') + else: + lines.append(f' public static VcpFeature {prop_name} => new({code}, "{name}", "{description}", VcpFeatureType.{feature_type}, VcpFeatureCategory.{category}, {supports_percentage});') + + return '\n'.join(lines) + +def generate_all_features_list(features): + """Generate the AllFeatures list.""" + lines = [ + " /// ", + " /// All features registry - contains all predefined MCCS features", + " /// ", + " public static IReadOnlyList AllFeatures { get; } = new List", + " {" + ] + + for i, feature in enumerate(features): + comma = ',' if i < len(features) - 1 else '' + lines.append(f" {feature['property']}{comma}") + + lines.append(" };") + return '\n'.join(lines) + +def main(): + script_dir = Path(__file__).parent + json_file = script_dir.parent / 'DDCSwitch' / 'VcpFeatureData.json' + output_file = script_dir.parent / 'DDCSwitch' / 'VcpFeature.Generated.cs' + + print(f"Loading {json_file}...") + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + features = data['features'] + print(f"Found {len(features)} features") + + # Generate the file + lines = [ + "// ", + "// This file is automatically generated from VcpFeatureData.json", + "// Do not edit this file directly. Instead, edit VcpFeatureData.json and regenerate.", + "", + "namespace DDCSwitch;", + "", + "public partial class VcpFeature", + "{", + " // ===== GENERATED VCP FEATURES =====" + ] + + # Group features by category + categories = {} + for feature in features: + category = feature['category'] + if category not in categories: + categories[category] = [] + categories[category].append(feature) + + # Generate properties grouped by category + for category, category_features in categories.items(): + lines.append("") + lines.append(f" // ===== {category.upper()} FEATURES =====") + lines.append("") + for feature in category_features: + lines.append(generate_feature_property(feature)) + lines.append("") + + # Generate AllFeatures list + lines.append("") + lines.append(generate_all_features_list(features)) + + lines.append("}") + lines.append("") + + content = '\n'.join(lines) + + print(f"Writing to {output_file}...") + with open(output_file, 'w', encoding='utf-8') as f: + f.write(content) + + print("Done!") + print(f"Generated {len(features)} feature properties") + +if __name__ == '__main__': + main() + diff --git a/tools/Regenerate.ps1 b/tools/Regenerate.ps1 new file mode 100644 index 0000000..763b982 --- /dev/null +++ b/tools/Regenerate.ps1 @@ -0,0 +1,30 @@ +# Regenerate VCP Feature Code +# Run this after editing VcpFeatureData.json + +Write-Host "=" * 60 +Write-Host "VCP Feature Code Generator" +Write-Host "=" * 60 + +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$projectRoot = Split-Path -Parent $scriptPath + +Push-Location $projectRoot + +try { + Write-Host "`nGenerating VcpFeature.Generated.cs from VcpFeatureData.json..." + python "$scriptPath\GenerateVcpFeatures.py" + + if ($LASTEXITCODE -eq 0) { + Write-Host "`n✓ Generation successful!" -ForegroundColor Green + Write-Host "`nNext steps:" + Write-Host " 1. Review the changes in VcpFeature.Generated.cs" + Write-Host " 2. Build the project: dotnet build" + Write-Host " 3. Test the changes" + } else { + Write-Host "`n✗ Generation failed!" -ForegroundColor Red + exit 1 + } +} finally { + Pop-Location +} +