Add brightness/contrast sliders, input source switching, and 9-language localization
- Add VCP commands for brightness (10), contrast (12), input source (60) - Fix UTF-16 encoding for monitor data parsing - Add system tray app with monitor controls - Add localization for en, es, fr, de, zh, ja, pt, it, hi - Update to .NET 9.0 - Add LICENSE and README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>CMM.Library</RootNamespace>
|
||||
<AssemblyName>CMM.Library</AssemblyName>
|
||||
|
||||
@@ -30,13 +30,13 @@ public static class CMMCommand
|
||||
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4");
|
||||
}
|
||||
|
||||
private static async Task<string> GetMonitorValue(string monitorSN, int? reTry = 0)
|
||||
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int? reTry = 0)
|
||||
{
|
||||
var value = string.Empty;
|
||||
while (reTry <= 5)
|
||||
{
|
||||
var cmdFileName = Path.Combine(CMMTmpFolder, $"{Guid.NewGuid()}.bat");
|
||||
var cmd = $"{CMMexe} /GetValue {monitorSN} D6\r\n" +
|
||||
var cmd = $"{CMMexe} /GetValue {monitorSN} {vcpCode}\r\n" +
|
||||
$"echo %errorlevel%";
|
||||
File.WriteAllText(cmdFileName, cmd);
|
||||
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
|
||||
@@ -46,12 +46,112 @@ public static class CMMCommand
|
||||
|
||||
if (!string.IsNullOrEmpty(value) && value != "0") return value;
|
||||
await Task.Delay(500);
|
||||
await GetMonitorValue(monitorSN, reTry++);
|
||||
await GetMonitorValue(monitorSN, vcpCode, reTry++);
|
||||
};
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
#region Brightness (VCP Code 10)
|
||||
|
||||
public static Task SetBrightness(string monitorSN, int value)
|
||||
{
|
||||
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 10 {value}");
|
||||
}
|
||||
|
||||
public static async Task<int?> GetBrightness(string monitorSN)
|
||||
{
|
||||
var value = await GetMonitorValue(monitorSN, "10");
|
||||
return int.TryParse(value, out var result) ? result : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Contrast (VCP Code 12)
|
||||
|
||||
public static Task SetContrast(string monitorSN, int value)
|
||||
{
|
||||
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 12 {value}");
|
||||
}
|
||||
|
||||
public static async Task<int?> GetContrast(string monitorSN)
|
||||
{
|
||||
var value = await GetMonitorValue(monitorSN, "12");
|
||||
return int.TryParse(value, out var result) ? result : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input Source (VCP Code 60)
|
||||
|
||||
public static Task SetInputSource(string monitorSN, int value)
|
||||
{
|
||||
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 60 {value}");
|
||||
}
|
||||
|
||||
public static async Task<int?> GetInputSource(string monitorSN)
|
||||
{
|
||||
var value = await GetMonitorValue(monitorSN, "60");
|
||||
return int.TryParse(value, out var result) ? result : null;
|
||||
}
|
||||
|
||||
public static async Task<List<InputSourceOption>> GetInputSourceOptions(string monitorSN)
|
||||
{
|
||||
var options = new List<InputSourceOption>();
|
||||
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp");
|
||||
|
||||
await ConsoleHelper.CmdCommandAsync($"{CMMexe} /sjson {savePath} {monitorSN}");
|
||||
|
||||
if (!File.Exists(savePath)) return options;
|
||||
|
||||
var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
|
||||
var inputSourceVcp = monitorModel?.FirstOrDefault(m => m.VCPCode == "60");
|
||||
|
||||
if (inputSourceVcp?.PossibleValues != null)
|
||||
{
|
||||
var possibleValues = inputSourceVcp.PossibleValues
|
||||
.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(v => int.TryParse(v.Trim(), out var val) ? val : (int?)null)
|
||||
.Where(v => v.HasValue)
|
||||
.Select(v => v.Value);
|
||||
|
||||
foreach (var value in possibleValues)
|
||||
{
|
||||
options.Add(new InputSourceOption(value, GetInputSourceName(value)));
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static string GetInputSourceName(int vcpValue)
|
||||
{
|
||||
return vcpValue switch
|
||||
{
|
||||
1 => "VGA-1",
|
||||
2 => "VGA-2",
|
||||
3 => "DVI-1",
|
||||
4 => "DVI-2",
|
||||
5 => "Composite-1",
|
||||
6 => "Composite-2",
|
||||
7 => "S-Video-1",
|
||||
8 => "S-Video-2",
|
||||
9 => "Tuner-1",
|
||||
10 => "Tuner-2",
|
||||
11 => "Tuner-3",
|
||||
12 => "Component-1",
|
||||
13 => "Component-2",
|
||||
14 => "Component-3",
|
||||
15 => "DisplayPort-1",
|
||||
16 => "DisplayPort-2",
|
||||
17 => "HDMI-1",
|
||||
18 => "HDMI-2",
|
||||
_ => $"Input-{vcpValue}"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static async Task<string> GetMonPowerStatus(string monitorSN)
|
||||
{
|
||||
var status = await GetMonitorValue(monitorSN);
|
||||
@@ -120,6 +220,9 @@ public static class CMMCommand
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string DebugLog = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "MonitorParse.log");
|
||||
|
||||
/// <summary>
|
||||
/// 取得螢幕清單
|
||||
/// </summary>
|
||||
@@ -129,55 +232,54 @@ public static class CMMCommand
|
||||
|
||||
if (!File.Exists(CMMsMonitors)) return monitors;
|
||||
|
||||
XMonitor mon = null;
|
||||
string context;
|
||||
foreach (var line in await File.ReadAllLinesAsync(CMMsMonitors))
|
||||
// Try reading raw bytes to understand encoding
|
||||
var rawBytes = await File.ReadAllBytesAsync(CMMsMonitors);
|
||||
File.WriteAllText(DebugLog, $"File size: {rawBytes.Length} bytes\nFirst 20 bytes: {BitConverter.ToString(rawBytes.Take(20).ToArray())}\n\n");
|
||||
|
||||
// Try UTF-16 LE (common Windows Unicode)
|
||||
var content = System.Text.Encoding.Unicode.GetString(rawBytes);
|
||||
File.AppendAllText(DebugLog, $"Content preview:\n{content.Substring(0, Math.Min(500, content.Length))}\n\n");
|
||||
|
||||
XMonitor? mon = null;
|
||||
foreach (var line in content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var sp = line.Split(":", StringSplitOptions.RemoveEmptyEntries);
|
||||
try
|
||||
{
|
||||
if (sp.Length != 2 || string.IsNullOrEmpty(sp[1])) continue;
|
||||
File.AppendAllText(DebugLog, $"LINE: [{line}]\n");
|
||||
|
||||
context = sp[1].Substring(2, sp[1].Length - 3);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var colonIdx = line.IndexOf(':');
|
||||
if (colonIdx < 0) continue;
|
||||
|
||||
if (sp[0].StartsWith("Monitor Device Name"))
|
||||
{
|
||||
mon = new XMonitor();
|
||||
mon.MonitorDeviceName = context;
|
||||
continue;
|
||||
}
|
||||
var key = line.Substring(0, colonIdx).Trim();
|
||||
var val = line.Substring(colonIdx + 1).Trim().Trim('"');
|
||||
|
||||
if (sp[0].StartsWith("Monitor Name"))
|
||||
{
|
||||
mon.MonitorName = context;
|
||||
continue;
|
||||
}
|
||||
File.AppendAllText(DebugLog, $" KEY=[{key}] VAL=[{val}]\n");
|
||||
|
||||
if (sp[0].StartsWith("Serial Number"))
|
||||
if (key.Contains("Monitor Device Name"))
|
||||
{
|
||||
mon.SerialNumber = context;
|
||||
continue;
|
||||
mon = new XMonitor { MonitorDeviceName = val };
|
||||
}
|
||||
|
||||
if (sp[0].StartsWith("Adapter Name"))
|
||||
else if (mon != null && key.Contains("Monitor Name"))
|
||||
{
|
||||
mon.AdapterName = context;
|
||||
continue;
|
||||
mon.MonitorName = val;
|
||||
}
|
||||
|
||||
if (sp[0].StartsWith("Monitor ID"))
|
||||
else if (mon != null && key.Contains("Serial Number"))
|
||||
{
|
||||
mon.MonitorID = context;
|
||||
monitors.Add(mon);
|
||||
continue;
|
||||
mon.SerialNumber = val;
|
||||
}
|
||||
else if (mon != null && key.Contains("Adapter Name"))
|
||||
{
|
||||
mon.AdapterName = val;
|
||||
}
|
||||
else if (mon != null && key.Contains("Monitor ID"))
|
||||
{
|
||||
mon.MonitorID = val;
|
||||
File.AppendAllText(DebugLog, $" -> Adding monitor: Name=[{mon.MonitorName}] SN=[{mon.SerialNumber}]\n");
|
||||
if (!string.IsNullOrEmpty(mon.SerialNumber))
|
||||
monitors.Add(mon);
|
||||
mon = null;
|
||||
}
|
||||
}
|
||||
|
||||
File.AppendAllText(DebugLog, $"\nTotal monitors with serial: {monitors.Count}\n");
|
||||
return monitors;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
namespace CMM.Library.ViewModel
|
||||
{
|
||||
public record InputSourceOption(int Value, string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class XMonitorStatus : PropertyBase
|
||||
{
|
||||
public string VCP_Code
|
||||
|
||||
Reference in New Issue
Block a user