refactor(ci): rename DellMonitorControl to MonitorControl
Rename project from DellMonitorControl to MonitorControl to reflect broader monitor compatibility beyond Dell hardware. Update all references in solution file, workflow, and project paths. Add DDC/CI rate limiting and throttling logic to prevent command failures: - Minimum 50ms interval between commands to same monitor - 8-second grace period after system resume before sending commands - 3-second cooldown after timeout to allow monitor recovery - Global semaphore to prevent command collisions Replace old icon with new generic monitor icon (ico and png formats).
This commit is contained in:
@@ -24,7 +24,7 @@ jobs:
|
|||||||
Write-Host "Setting version to: $version"
|
Write-Host "Setting version to: $version"
|
||||||
|
|
||||||
# Update csproj (preserve UTF-8 BOM encoding)
|
# Update csproj (preserve UTF-8 BOM encoding)
|
||||||
$csprojPath = "DellMonitorControl/DellMonitorControl.csproj"
|
$csprojPath = "MonitorControl/MonitorControl.csproj"
|
||||||
$content = [System.IO.File]::ReadAllText($csprojPath)
|
$content = [System.IO.File]::ReadAllText($csprojPath)
|
||||||
$content = $content -replace '<Version>.*</Version>', "<Version>$version</Version>"
|
$content = $content -replace '<Version>.*</Version>', "<Version>$version</Version>"
|
||||||
$content = $content -replace '<AssemblyVersion>.*</AssemblyVersion>', "<AssemblyVersion>$version.0</AssemblyVersion>"
|
$content = $content -replace '<AssemblyVersion>.*</AssemblyVersion>', "<AssemblyVersion>$version.0</AssemblyVersion>"
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
[System.IO.File]::WriteAllText($csprojPath, $content, [System.Text.UTF8Encoding]::new($true))
|
[System.IO.File]::WriteAllText($csprojPath, $content, [System.Text.UTF8Encoding]::new($true))
|
||||||
|
|
||||||
# Update ISS
|
# Update ISS
|
||||||
$issPath = "DellMonitorControl/MonitorControl.iss"
|
$issPath = "MonitorControl/MonitorControl.iss"
|
||||||
$issContent = [System.IO.File]::ReadAllText($issPath)
|
$issContent = [System.IO.File]::ReadAllText($issPath)
|
||||||
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
|
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
|
||||||
[System.IO.File]::WriteAllText($issPath, $issContent, [System.Text.UTF8Encoding]::new($true))
|
[System.IO.File]::WriteAllText($issPath, $issContent, [System.Text.UTF8Encoding]::new($true))
|
||||||
@@ -48,21 +48,21 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Current directory:"
|
echo "Current directory:"
|
||||||
cd
|
cd
|
||||||
echo "DellMonitorControl contents:"
|
echo "MonitorControl contents:"
|
||||||
dir DellMonitorControl
|
dir MonitorControl
|
||||||
echo "DellMonitorControl\bin contents:"
|
echo "MonitorControl\bin contents:"
|
||||||
dir DellMonitorControl\bin
|
dir MonitorControl\bin
|
||||||
echo "DellMonitorControl\bin\Release contents:"
|
echo "MonitorControl\bin\Release contents:"
|
||||||
dir DellMonitorControl\bin\Release
|
dir MonitorControl\bin\Release
|
||||||
echo "DellMonitorControl\bin\Release\net9.0-windows contents:"
|
echo "MonitorControl\bin\Release\net9.0-windows contents:"
|
||||||
dir DellMonitorControl\bin\Release\net9.0-windows
|
dir MonitorControl\bin\Release\net9.0-windows
|
||||||
|
|
||||||
- name: Build Installer
|
- name: Build Installer
|
||||||
shell: cmd
|
shell: cmd
|
||||||
run: |
|
run: |
|
||||||
echo Copying to short path to avoid path length issues...
|
echo Copying to short path to avoid path length issues...
|
||||||
mkdir C:\build 2>nul
|
mkdir C:\build 2>nul
|
||||||
xcopy /E /I /Y DellMonitorControl C:\build\app
|
xcopy /E /I /Y MonitorControl C:\build\app
|
||||||
echo.
|
echo.
|
||||||
echo Working from C:\build\app
|
echo Working from C:\build\app
|
||||||
cd /d C:\build\app
|
cd /d C:\build\app
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$version = "${{ gitea.ref_name }}".TrimStart("v")
|
$version = "${{ gitea.ref_name }}".TrimStart("v")
|
||||||
$buildDir = "DellMonitorControl\bin\Release\net9.0-windows"
|
$buildDir = "MonitorControl\bin\Release\net9.0-windows"
|
||||||
$zipName = "MonitorControl-Portable-$version.zip"
|
$zipName = "MonitorControl-Portable-$version.zip"
|
||||||
|
|
||||||
Write-Host "Creating portable zip: $zipName"
|
Write-Host "Creating portable zip: $zipName"
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
$body = @{
|
$body = @{
|
||||||
tag_name = $tag
|
tag_name = $tag
|
||||||
name = "Monitor Control $tag"
|
name = "Monitor Control $tag"
|
||||||
body = "## Monitor Control $tag`n`n### Installation`n- **Installer**: Download and run the setup exe`n- **Portable**: Download the zip, extract anywhere, and run DellMonitorControl.exe`n`n### Features`n- System tray monitor control`n- Brightness/Contrast adjustment`n- Input source switching`n- Quick-switch toolbar`n`n### Requirements`n- Windows 10/11`n- .NET 9.0 Runtime"
|
body = "## Monitor Control $tag`n`n### Installation`n- **Installer**: Download and run the setup exe`n- **Portable**: Download the zip, extract anywhere, and run MonitorControl.exe`n`n### Features`n- System tray monitor control`n- Brightness/Contrast adjustment`n- Input source switching`n- Quick-switch toolbar`n`n### Requirements`n- Windows 10/11`n- .NET 9.0 Runtime"
|
||||||
} | ConvertTo-Json
|
} | ConvertTo-Json
|
||||||
|
|
||||||
$release = Invoke-RestMethod -Uri "$baseUrl/repos/$repo/releases" -Method Post -Headers @{"Authorization"="token $env:GITEA_TOKEN"; "Content-Type"="application/json"} -Body $body
|
$release = Invoke-RestMethod -Uri "$baseUrl/repos/$repo/releases" -Method Post -Headers @{"Authorization"="token $env:GITEA_TOKEN"; "Content-Type"="application/json"} -Body $body
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Language", "Language\Langua
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{0D34DD73-3282-40EB-8F59-DF190944BF12}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{0D34DD73-3282-40EB-8F59-DF190944BF12}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DellMonitorControl", "DellMonitorControl\DellMonitorControl.csproj", "{64E96610-D431-40B9-A00B-55CE195B4B58}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitorControl", "MonitorControl\MonitorControl.csproj", "{64E96610-D431-40B9-A00B-55CE195B4B58}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CMMService", "CMMService\CMMService.csproj", "{FEA2B019-32BC-4704-939F-1CD26F373F55}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CMMService", "CMMService\CMMService.csproj", "{FEA2B019-32BC-4704-939F-1CD26F373F55}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,6 +1,7 @@
|
|||||||
using CMM.Library.Base;
|
using CMM.Library.Base;
|
||||||
using CMM.Library.Helpers;
|
using CMM.Library.Helpers;
|
||||||
using CMM.Library.ViewModel;
|
using CMM.Library.ViewModel;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace CMM.Library.Method;
|
namespace CMM.Library.Method;
|
||||||
@@ -14,6 +15,78 @@ public static class CMMCommand
|
|||||||
static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe");
|
static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe");
|
||||||
static readonly string CMMsMonitors = Path.Combine(CMMTmpFolder, "smonitors.tmp");
|
static readonly string CMMsMonitors = Path.Combine(CMMTmpFolder, "smonitors.tmp");
|
||||||
|
|
||||||
|
// DDC/CI rate limiting: minimum time between commands to the same monitor
|
||||||
|
private static readonly ConcurrentDictionary<string, DateTime> _lastCommandTime = new();
|
||||||
|
private static readonly SemaphoreSlim _globalDdcLock = new(1, 1);
|
||||||
|
private const int MinCommandIntervalMs = 50;
|
||||||
|
|
||||||
|
// Post-resume grace period: prevents DDC/CI commands from hitting monitors still waking up
|
||||||
|
private static DateTime _resumeTime = DateTime.MinValue;
|
||||||
|
private const int ResumeGracePeriodMs = 8000;
|
||||||
|
|
||||||
|
// Timeout cooldown: when a command times out, the monitor is likely struggling.
|
||||||
|
// Back off for this long before sending another command to that monitor.
|
||||||
|
private static readonly ConcurrentDictionary<string, DateTime> _timeoutCooldowns = new();
|
||||||
|
private const int TimeoutCooldownMs = 3000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Call this when the system resumes from sleep/hibernate to activate the grace period.
|
||||||
|
/// Monitors need several seconds after resume before their scalers are ready for DDC/CI.
|
||||||
|
/// </summary>
|
||||||
|
public static void NotifySystemResumed()
|
||||||
|
{
|
||||||
|
_resumeTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for the post-resume grace period to expire, respects timeout cooldowns,
|
||||||
|
/// and enforces per-monitor command spacing.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task ThrottleDdcCommand(string monitorSN)
|
||||||
|
{
|
||||||
|
// Wait for post-resume grace period
|
||||||
|
var elapsed = (DateTime.UtcNow - _resumeTime).TotalMilliseconds;
|
||||||
|
if (elapsed < ResumeGracePeriodMs)
|
||||||
|
{
|
||||||
|
var waitMs = (int)(ResumeGracePeriodMs - elapsed);
|
||||||
|
await Task.Delay(waitMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for timeout cooldown — if this monitor recently timed out, give it time to recover
|
||||||
|
if (_timeoutCooldowns.TryGetValue(monitorSN, out var cooldownUntil))
|
||||||
|
{
|
||||||
|
var remaining = (cooldownUntil - DateTime.UtcNow).TotalMilliseconds;
|
||||||
|
if (remaining > 0)
|
||||||
|
await Task.Delay((int)remaining);
|
||||||
|
_timeoutCooldowns.TryRemove(monitorSN, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce minimum interval between commands to the same monitor
|
||||||
|
await _globalDdcLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_lastCommandTime.TryGetValue(monitorSN, out var lastTime))
|
||||||
|
{
|
||||||
|
var sinceLastMs = (DateTime.UtcNow - lastTime).TotalMilliseconds;
|
||||||
|
if (sinceLastMs < MinCommandIntervalMs)
|
||||||
|
await Task.Delay((int)(MinCommandIntervalMs - sinceLastMs));
|
||||||
|
}
|
||||||
|
_lastCommandTime[monitorSN] = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_globalDdcLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a command to this monitor timed out, activating a cooldown period.
|
||||||
|
/// </summary>
|
||||||
|
private static void RecordTimeout(string monitorSN)
|
||||||
|
{
|
||||||
|
_timeoutCooldowns[monitorSN] = DateTime.UtcNow.AddMilliseconds(TimeoutCooldownMs);
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task ScanMonitor()
|
public static async Task ScanMonitor()
|
||||||
{
|
{
|
||||||
// Ensure temp folder exists for output files
|
// Ensure temp folder exists for output files
|
||||||
@@ -23,26 +96,36 @@ public static class CMMCommand
|
|||||||
|
|
||||||
public static async Task PowerOn(string monitorSN)
|
public static async Task PowerOn(string monitorSN)
|
||||||
{
|
{
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 1");
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 1");
|
||||||
|
if (exitCode == -1) RecordTimeout(monitorSN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Sleep(string monitorSN)
|
public static async Task Sleep(string monitorSN)
|
||||||
{
|
{
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 4");
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} D6 4");
|
||||||
|
if (exitCode == -1) RecordTimeout(monitorSN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2)
|
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2)
|
||||||
{
|
{
|
||||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||||
{
|
{
|
||||||
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
|
||||||
// Execute directly without batch file wrapper
|
// Execute directly without batch file wrapper
|
||||||
var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync(
|
var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync(
|
||||||
CMMexe,
|
CMMexe,
|
||||||
$"/GetValue {monitorSN} {vcpCode}");
|
$"/GetValue {monitorSN} {vcpCode}");
|
||||||
|
|
||||||
// Timeout
|
// Timeout — monitor is unresponsive, activate cooldown and stop retrying.
|
||||||
|
// Continuing to send commands to a timed-out monitor risks scaler lockout.
|
||||||
if (exitCode == -1)
|
if (exitCode == -1)
|
||||||
|
{
|
||||||
|
RecordTimeout(monitorSN);
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
// ControlMyMonitor returns the value as the exit code
|
// ControlMyMonitor returns the value as the exit code
|
||||||
// Exit code > 0 means success with that value
|
// Exit code > 0 means success with that value
|
||||||
@@ -66,7 +149,9 @@ public static class CMMCommand
|
|||||||
|
|
||||||
public static async Task SetBrightness(string monitorSN, int value)
|
public static async Task SetBrightness(string monitorSN, int value)
|
||||||
{
|
{
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 10 {value}");
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 10 {value}");
|
||||||
|
if (exitCode == -1) RecordTimeout(monitorSN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<int?> GetBrightness(string monitorSN)
|
public static async Task<int?> GetBrightness(string monitorSN)
|
||||||
@@ -81,7 +166,9 @@ public static class CMMCommand
|
|||||||
|
|
||||||
public static async Task SetContrast(string monitorSN, int value)
|
public static async Task SetContrast(string monitorSN, int value)
|
||||||
{
|
{
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 12 {value}");
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 12 {value}");
|
||||||
|
if (exitCode == -1) RecordTimeout(monitorSN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<int?> GetContrast(string monitorSN)
|
public static async Task<int?> GetContrast(string monitorSN)
|
||||||
@@ -96,7 +183,9 @@ public static class CMMCommand
|
|||||||
|
|
||||||
public static async Task SetInputSource(string monitorSN, int value)
|
public static async Task SetInputSource(string monitorSN, int value)
|
||||||
{
|
{
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 60 {value}");
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/SetValue {monitorSN} 60 {value}");
|
||||||
|
if (exitCode == -1) RecordTimeout(monitorSN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<int?> GetInputSource(string monitorSN)
|
public static async Task<int?> GetInputSource(string monitorSN)
|
||||||
@@ -110,7 +199,9 @@ public static class CMMCommand
|
|||||||
var options = new List<InputSourceOption>();
|
var options = new List<InputSourceOption>();
|
||||||
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp");
|
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp");
|
||||||
|
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {monitorSN}");
|
await ThrottleDdcCommand(monitorSN);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {monitorSN}");
|
||||||
|
if (exitCode == -1) RecordTimeout(monitorSN);
|
||||||
|
|
||||||
if (!File.Exists(savePath)) return options;
|
if (!File.Exists(savePath)) return options;
|
||||||
|
|
||||||
@@ -177,17 +268,19 @@ public static class CMMCommand
|
|||||||
|
|
||||||
public static async Task ScanMonitorStatus(IEnumerable<XMonitor> monitors)
|
public static async Task ScanMonitorStatus(IEnumerable<XMonitor> monitors)
|
||||||
{
|
{
|
||||||
var taskList = monitors.Select(x =>
|
// Sequential scan to avoid overwhelming DDC/CI bus with parallel commands
|
||||||
|
foreach (var x in monitors)
|
||||||
{
|
{
|
||||||
return ScanMonitorStatus($"{CMMTmpFolder}\\{x.SerialNumber}.tmp", x);
|
await ScanMonitorStatus($"{CMMTmpFolder}\\{x.SerialNumber}.tmp", x);
|
||||||
});
|
}
|
||||||
|
|
||||||
await Task.WhenAll(taskList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task ScanMonitorStatus(string savePath, XMonitor mon)
|
static async Task ScanMonitorStatus(string savePath, XMonitor mon)
|
||||||
{
|
{
|
||||||
await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {mon.MonitorID}");
|
var sn = mon.SerialNumber ?? mon.MonitorID ?? "unknown";
|
||||||
|
await ThrottleDdcCommand(sn);
|
||||||
|
var (_, exitCode) = await ConsoleHelper.ExecuteExeAsync(CMMexe, $"/sjson {savePath} {mon.MonitorID}");
|
||||||
|
if (exitCode == -1) RecordTimeout(sn);
|
||||||
var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
|
var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
|
||||||
|
|
||||||
var status = monitorModel.ReadMonitorStatus();
|
var status = monitorModel.ReadMonitorStatus();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<Application x:Class="DellMonitorControl.App"
|
<Application x:Class="MonitorControl.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6"
|
xmlns:tb="clr-namespace:Hardcodet.Wpf.TaskbarNotification;assembly=Hardcodet.Wpf.TaskbarNotification.Net6"
|
||||||
xmlns:local="clr-namespace:DellMonitorControl"
|
xmlns:local="clr-namespace:MonitorControl"
|
||||||
Startup="Application_Startup">
|
Startup="Application_Startup">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<tb:TaskbarIcon x:Key="TrayIcon"
|
<tb:TaskbarIcon x:Key="TrayIcon"
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
using CMM.Library.Method;
|
||||||
using Hardcodet.Wpf.TaskbarNotification;
|
using Hardcodet.Wpf.TaskbarNotification;
|
||||||
|
using Microsoft.Win32;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace DellMonitorControl
|
namespace MonitorControl
|
||||||
{
|
{
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
@@ -17,10 +19,23 @@ namespace DellMonitorControl
|
|||||||
|
|
||||||
_mainWindow = new MainWindow();
|
_mainWindow = new MainWindow();
|
||||||
|
|
||||||
|
// Listen for system sleep/resume to protect monitors during wake
|
||||||
|
SystemEvents.PowerModeChanged += OnPowerModeChanged;
|
||||||
|
|
||||||
// Check for updates in background
|
// Check for updates in background
|
||||||
await CheckForUpdatesAsync();
|
await CheckForUpdatesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Mode == PowerModes.Resume)
|
||||||
|
{
|
||||||
|
// Notify the DDC/CI layer that monitors need time to wake up
|
||||||
|
CMMCommand.NotifySystemResumed();
|
||||||
|
DebugLogger.Log("System resumed from sleep - DDC/CI grace period activated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task CheckForUpdatesAsync()
|
private async System.Threading.Tasks.Task CheckForUpdatesAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -62,6 +77,7 @@ namespace DellMonitorControl
|
|||||||
|
|
||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
|
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
|
||||||
_trayIcon?.Dispose();
|
_trayIcon?.Dispose();
|
||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Window x:Class="DellMonitorControl.ConfigWindow"
|
<Window x:Class="MonitorControl.ConfigWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="Port Configuration"
|
Title="Port Configuration"
|
||||||
@@ -4,12 +4,13 @@ using CMM.Library.ViewModel;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace DellMonitorControl;
|
namespace MonitorControl;
|
||||||
|
|
||||||
public partial class ConfigWindow : Window
|
public partial class ConfigWindow : Window
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,7 @@ public partial class ConfigWindow : Window
|
|||||||
private readonly string _monitorName;
|
private readonly string _monitorName;
|
||||||
private readonly List<InputSourceOption> _availablePorts;
|
private readonly List<InputSourceOption> _availablePorts;
|
||||||
private readonly List<PortConfigRow> _portRows = new();
|
private readonly List<PortConfigRow> _portRows = new();
|
||||||
|
private CancellationTokenSource? _detectCts;
|
||||||
|
|
||||||
public bool ConfigChanged { get; private set; }
|
public bool ConfigChanged { get; private set; }
|
||||||
|
|
||||||
@@ -205,13 +207,35 @@ public partial class ConfigWindow : Window
|
|||||||
|
|
||||||
private async void DetectButton_Click(object sender, RoutedEventArgs e)
|
private async void DetectButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
btnDetect.IsEnabled = false;
|
// If detection is already running, cancel it
|
||||||
btnDetect.Content = "...";
|
if (_detectCts != null)
|
||||||
|
{
|
||||||
|
_detectCts.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn the user about what this does
|
||||||
|
var confirm = MessageBox.Show(
|
||||||
|
"Port detection will temporarily switch your monitor's input source to probe for available ports. " +
|
||||||
|
"Your screen may go dark briefly during each probe.\n\n" +
|
||||||
|
"This is safe for most monitors, but avoid running it repeatedly in a short time.\n\n" +
|
||||||
|
"You can click the Cancel button to stop detection at any time.\n\n" +
|
||||||
|
"Continue?",
|
||||||
|
"Port Detection", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
if (confirm != MessageBoxResult.Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_detectCts = new CancellationTokenSource();
|
||||||
|
var ct = _detectCts.Token;
|
||||||
|
|
||||||
|
btnDetect.Content = "Cancel";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Common VCP 60 values: 15=DP1, 16=DP2, 17=HDMI1, 18=HDMI2, 3=DVI1, 4=DVI2, 1=VGA1, 2=VGA2
|
// Only probe modern ports (DP/HDMI) by default — legacy ports (VGA/DVI) are rarely used
|
||||||
var commonPorts = new[] { 15, 16, 17, 18, 3, 4, 1, 2 };
|
// and probing them on monitors without those connectors wastes time and stresses the scaler
|
||||||
|
var commonPorts = new[] { 15, 16, 17, 18 };
|
||||||
var detectedPorts = new List<int>();
|
var detectedPorts = new List<int>();
|
||||||
|
|
||||||
// Get current input so we can restore it
|
// Get current input so we can restore it
|
||||||
@@ -219,6 +243,8 @@ public partial class ConfigWindow : Window
|
|||||||
|
|
||||||
foreach (var vcpValue in commonPorts)
|
foreach (var vcpValue in commonPorts)
|
||||||
{
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// Skip ports we already know about
|
// Skip ports we already know about
|
||||||
if (_availablePorts.Any(p => p.Value == vcpValue))
|
if (_availablePorts.Any(p => p.Value == vcpValue))
|
||||||
continue;
|
continue;
|
||||||
@@ -227,7 +253,10 @@ public partial class ConfigWindow : Window
|
|||||||
{
|
{
|
||||||
// Try to set the input - if it succeeds, the port exists
|
// Try to set the input - if it succeeds, the port exists
|
||||||
await CMMCommand.SetInputSource(_serialNumber, vcpValue);
|
await CMMCommand.SetInputSource(_serialNumber, vcpValue);
|
||||||
await Task.Delay(500); // Give monitor time to respond
|
|
||||||
|
// Give the monitor's scaler time to attempt signal lock on the new input.
|
||||||
|
// 3 seconds is the minimum safe settle time for most scalers.
|
||||||
|
await Task.Delay(3000, ct);
|
||||||
|
|
||||||
// Check if the input actually changed
|
// Check if the input actually changed
|
||||||
var newInput = await CMMCommand.GetInputSource(_serialNumber);
|
var newInput = await CMMCommand.GetInputSource(_serialNumber);
|
||||||
@@ -236,21 +265,26 @@ public partial class ConfigWindow : Window
|
|||||||
detectedPorts.Add(vcpValue);
|
detectedPorts.Add(vcpValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException) { throw; }
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Port doesn't exist or isn't supported
|
// Port doesn't exist or isn't supported — continue to next
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore original input if we have one
|
// Extra settle time between probes to let the scaler fully recover
|
||||||
|
try { await Task.Delay(1000, ct); }
|
||||||
|
catch (OperationCanceledException) { throw; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original input — give it extra time to settle after all the switching
|
||||||
if (currentInput.HasValue)
|
if (currentInput.HasValue)
|
||||||
{
|
{
|
||||||
await CMMCommand.SetInputSource(_serialNumber, currentInput.Value);
|
await CMMCommand.SetInputSource(_serialNumber, currentInput.Value);
|
||||||
|
await Task.Delay(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detectedPorts.Count > 0)
|
if (detectedPorts.Count > 0)
|
||||||
{
|
{
|
||||||
// Add detected ports to the available list and config
|
|
||||||
foreach (var vcpValue in detectedPorts)
|
foreach (var vcpValue in detectedPorts)
|
||||||
{
|
{
|
||||||
var name = CMMCommand.GetInputSourceName(vcpValue);
|
var name = CMMCommand.GetInputSourceName(vcpValue);
|
||||||
@@ -258,7 +292,6 @@ public partial class ConfigWindow : Window
|
|||||||
MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name);
|
MonitorConfigManager.AddDiscoveredPort(_serialNumber, _monitorName, vcpValue, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload to show new ports
|
|
||||||
MonitorConfigManager.ClearCache();
|
MonitorConfigManager.ClearCache();
|
||||||
LoadPortConfiguration();
|
LoadPortConfiguration();
|
||||||
|
|
||||||
@@ -272,6 +305,21 @@ public partial class ConfigWindow : Window
|
|||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// User cancelled — try to restore original input before exiting
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentInput = await CMMCommand.GetInputSource(_serialNumber);
|
||||||
|
// Only restore if we can still talk to the monitor
|
||||||
|
if (currentInput.HasValue)
|
||||||
|
await CMMCommand.SetInputSource(_serialNumber, currentInput.Value);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
MessageBox.Show("Port detection was cancelled.", "Detection Cancelled",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Detection failed: {ex.Message}", "Error",
|
MessageBox.Show($"Detection failed: {ex.Message}", "Error",
|
||||||
@@ -279,8 +327,9 @@ public partial class ConfigWindow : Window
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
_detectCts?.Dispose();
|
||||||
|
_detectCts = null;
|
||||||
btnDetect.Content = "Detect";
|
btnDetect.Content = "Detect";
|
||||||
btnDetect.IsEnabled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<UserControl x:Class="DellMonitorControl.ControlPanel"
|
<UserControl x:Class="MonitorControl.ControlPanel"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
@@ -9,7 +9,7 @@ using System.Windows;
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace DellMonitorControl;
|
namespace MonitorControl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interaction logic for ControlPanel.xaml
|
/// Interaction logic for ControlPanel.xaml
|
||||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DellMonitorControl;
|
namespace MonitorControl;
|
||||||
|
|
||||||
public static class DebugLogger
|
public static class DebugLogger
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Window x:Class="DellMonitorControl.MainWindow"
|
<Window x:Class="MonitorControl.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="Monitor Control"
|
Title="Monitor Control"
|
||||||
@@ -7,11 +7,12 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Interop;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Animation;
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
namespace DellMonitorControl;
|
namespace MonitorControl;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
@@ -21,13 +22,51 @@ public partial class MainWindow : Window
|
|||||||
private UpdateInfo? _pendingUpdate;
|
private UpdateInfo? _pendingUpdate;
|
||||||
private DateTime _lastUpdateCheck = DateTime.MinValue;
|
private DateTime _lastUpdateCheck = DateTime.MinValue;
|
||||||
|
|
||||||
|
// WM_DISPLAYCHANGE: sent when display resolution changes or monitors are connected/disconnected
|
||||||
|
private const int WM_DISPLAYCHANGE = 0x007E;
|
||||||
|
private bool _displayChangeReloadPending;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation");
|
_spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation");
|
||||||
|
SourceInitialized += MainWindow_SourceInitialized;
|
||||||
DebugLogger.Log("MainWindow initialized");
|
DebugLogger.Log("MainWindow initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
|
||||||
|
source?.AddHook(WndProc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
if (msg == WM_DISPLAYCHANGE && !_displayChangeReloadPending)
|
||||||
|
{
|
||||||
|
_displayChangeReloadPending = true;
|
||||||
|
DebugLogger.Log("WM_DISPLAYCHANGE received - scheduling monitor refresh");
|
||||||
|
|
||||||
|
// Debounce: display changes often fire multiple messages in quick succession.
|
||||||
|
// Wait 2 seconds for things to settle, then reload if window is visible.
|
||||||
|
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
||||||
|
timer.Tick += async (s, args) =>
|
||||||
|
{
|
||||||
|
timer.Stop();
|
||||||
|
_displayChangeReloadPending = false;
|
||||||
|
|
||||||
|
if (IsVisible)
|
||||||
|
{
|
||||||
|
DebugLogger.Log("Reloading monitors after display change");
|
||||||
|
await LoadMonitors();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowNearTray()
|
public void ShowNearTray()
|
||||||
{
|
{
|
||||||
DebugLogger.Log("ShowNearTray called");
|
DebugLogger.Log("ShowNearTray called");
|
||||||
@@ -119,7 +158,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
Title = "About Monitor Control",
|
Title = "About Monitor Control",
|
||||||
Width = 300,
|
Width = 300,
|
||||||
Height = 180,
|
Height = 240,
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||||
ResizeMode = ResizeMode.NoResize,
|
ResizeMode = ResizeMode.NoResize,
|
||||||
WindowStyle = WindowStyle.None,
|
WindowStyle = WindowStyle.None,
|
||||||
@@ -138,6 +177,16 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||||
|
|
||||||
|
var logo = new System.Windows.Controls.Image
|
||||||
|
{
|
||||||
|
Source = new System.Windows.Media.Imaging.BitmapImage(new Uri("pack://application:,,,/MonitorIcon.png")),
|
||||||
|
Width = 48,
|
||||||
|
Height = 48,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 0, 10)
|
||||||
|
};
|
||||||
|
stack.Children.Add(logo);
|
||||||
|
|
||||||
stack.Children.Add(new TextBlock
|
stack.Children.Add(new TextBlock
|
||||||
{
|
{
|
||||||
Text = "Monitor Control",
|
Text = "Monitor Control",
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net9.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>DellMonitorControl</AssemblyName>
|
<AssemblyName>MonitorControl</AssemblyName>
|
||||||
<RootNamespace>DellMonitorControl</RootNamespace>
|
<RootNamespace>MonitorControl</RootNamespace>
|
||||||
<Product>ControlMyMonitorManagement</Product>
|
<Product>ControlMyMonitorManagement</Product>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<Company>MarketAlly</Company>
|
<Company>MarketAlly</Company>
|
||||||
@@ -22,10 +22,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="MonitorIcon.ico" />
|
<None Remove="MonitorIcon.ico" />
|
||||||
|
<None Remove="MonitorIcon.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="MonitorIcon.ico" />
|
<Resource Include="MonitorIcon.ico" />
|
||||||
|
<Resource Include="MonitorIcon.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#define MyAppName "Monitor Control"
|
#define MyAppName "Monitor Control"
|
||||||
#define MyAppVersion "1.1.5"
|
#define MyAppVersion "1.1.5"
|
||||||
#define MyAppPublisher "MarketAlly"
|
#define MyAppPublisher "MarketAlly"
|
||||||
#define MyAppExeName "DellMonitorControl.exe"
|
#define MyAppExeName "MonitorControl.exe"
|
||||||
#define MyAppIcon "MonitorIcon.ico"
|
#define MyAppIcon "MonitorIcon.ico"
|
||||||
#define SourcePath "bin\Release\net9.0-windows"
|
#define SourcePath "bin\Release\net9.0-windows"
|
||||||
|
|
||||||
BIN
MonitorControl/MonitorIcon.ico
Normal file
BIN
MonitorControl/MonitorIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
MonitorControl/MonitorIcon.png
Normal file
BIN
MonitorControl/MonitorIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@@ -5,7 +5,7 @@ using System.Reflection;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DellMonitorControl;
|
namespace MonitorControl;
|
||||||
|
|
||||||
public class UpdateChecker
|
public class UpdateChecker
|
||||||
{
|
{
|
||||||
Reference in New Issue
Block a user