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:
@@ -1,11 +1,12 @@
|
||||
using CMM.Library.Base;
|
||||
using CMM.Library.Helpers;
|
||||
using CMM.Library.ViewModel;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
|
||||
namespace CMM.Library.Method;
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Control My Monitor Management Command
|
||||
/// </summary>
|
||||
public static class CMMCommand
|
||||
@@ -14,6 +15,78 @@ public static class CMMCommand
|
||||
static readonly string CMMexe = Path.Combine(AppContext.BaseDirectory, "ControlMyMonitor.exe");
|
||||
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()
|
||||
{
|
||||
// Ensure temp folder exists for output files
|
||||
@@ -23,26 +96,36 @@ public static class CMMCommand
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
await ThrottleDdcCommand(monitorSN);
|
||||
|
||||
// Execute directly without batch file wrapper
|
||||
var (output, exitCode) = await ConsoleHelper.ExecuteExeAsync(
|
||||
CMMexe,
|
||||
$"/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)
|
||||
{
|
||||
RecordTimeout(monitorSN);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// ControlMyMonitor returns the value as the exit code
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
@@ -81,7 +166,9 @@ public static class CMMCommand
|
||||
|
||||
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)
|
||||
@@ -96,7 +183,9 @@ public static class CMMCommand
|
||||
|
||||
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)
|
||||
@@ -110,7 +199,9 @@ public static class CMMCommand
|
||||
var options = new List<InputSourceOption>();
|
||||
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;
|
||||
|
||||
@@ -177,17 +268,19 @@ public static class CMMCommand
|
||||
|
||||
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 Task.WhenAll(taskList);
|
||||
await ScanMonitorStatus($"{CMMTmpFolder}\\{x.SerialNumber}.tmp", x);
|
||||
}
|
||||
}
|
||||
|
||||
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 status = monitorModel.ReadMonitorStatus();
|
||||
|
||||
Reference in New Issue
Block a user