2
0

refactor(ci): rename DellMonitorControl to MonitorControl
All checks were successful
Build / build (push) Successful in 9h0m26s
Build and Release / build (push) Successful in 8h0m15s

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:
2026-01-29 18:14:58 -05:00
parent 3c6cc15281
commit 404e6f42da
24 changed files with 262 additions and 53 deletions

View File

@@ -0,0 +1,492 @@
using CMM.Language;
using CMM.Library.Method;
using CMM.Library.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace MonitorControl;
/// <summary>
/// Interaction logic for ControlPanel.xaml
/// </summary>
public partial class ControlPanel : UserControl
{
public ControlPanel()
{
InitializeComponent();
}
public async Task Refresh()
{
try
{
sp.Children.Clear();
await CMMCommand.ScanMonitor();
var monitors = (await CMMCommand.ReadMonitorsData()).ToList();
if (!monitors.Any())
{
sp.Children.Add(new TextBlock
{
Text = "No DDC/CI monitors detected",
Foreground = System.Windows.Media.Brushes.White,
FontSize = 14
});
return;
}
foreach (var m in monitors)
{
var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50;
var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50;
var inputSource = await CMMCommand.GetInputSource(m.SerialNumber);
var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber);
var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown";
// Monitor name header
sp.Children.Add(new TextBlock
{
Text = m.MonitorName,
Foreground = System.Windows.Media.Brushes.White,
FontSize = 14,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 10, 0, 5)
});
// Brightness row
var brightnessRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
brightnessRow.Children.Add(new TextBlock { Text = "Brightness", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
var bSlider = new Slider { Minimum = 0, Maximum = 100, Value = brightness, Width = 140, VerticalAlignment = VerticalAlignment.Center, Tag = m.SerialNumber };
var bValue = new TextBlock { Text = brightness.ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
bSlider.ValueChanged += (s, e) => bValue.Text = ((int)e.NewValue).ToString();
bSlider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await CMMCommand.SetBrightness(sn, (int)sl.Value); };
brightnessRow.Children.Add(bSlider);
brightnessRow.Children.Add(bValue);
sp.Children.Add(brightnessRow);
// Contrast row
var contrastRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
contrastRow.Children.Add(new TextBlock { Text = "Contrast", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
var cSlider = new Slider { Minimum = 0, Maximum = 100, Value = contrast, Width = 140, VerticalAlignment = VerticalAlignment.Center, Tag = m.SerialNumber };
var cValue = new TextBlock { Text = contrast.ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
cSlider.ValueChanged += (s, e) => cValue.Text = ((int)e.NewValue).ToString();
cSlider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await CMMCommand.SetContrast(sn, (int)sl.Value); };
contrastRow.Children.Add(cSlider);
contrastRow.Children.Add(cValue);
sp.Children.Add(contrastRow);
// Input source row (if options available)
if (inputOptions.Count > 0)
{
var inputRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
inputRow.Children.Add(new TextBlock { Text = "Input", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
var combo = new ComboBox { Width = 170, ItemsSource = inputOptions, DisplayMemberPath = "Name", Tag = m.SerialNumber };
if (inputSource.HasValue) combo.SelectedItem = inputOptions.Find(o => o.Value == inputSource.Value);
combo.SelectionChanged += async (s, e) => { if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt) await CMMCommand.SetInputSource(sn, opt.Value); };
inputRow.Children.Add(combo);
sp.Children.Add(inputRow);
}
// Power row
var powerRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) };
powerRow.Children.Add(new TextBlock { Text = "Power", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center });
var powerBtn = new Button { Content = powerStatus, Width = 170, Tag = m.SerialNumber };
powerBtn.Click += async (s, e) =>
{
if (s is Button btn && btn.Tag is string sn)
{
var status = await CMMCommand.GetMonPowerStatus(sn);
if (status == "Sleep" || status == "PowerOff") await CMMCommand.PowerOn(sn);
else await CMMCommand.Sleep(sn);
await Task.Delay(1000);
btn.Content = await CMMCommand.GetMonPowerStatus(sn);
}
};
powerRow.Children.Add(powerBtn);
sp.Children.Add(powerRow);
}
}
catch (Exception ex)
{
sp.Children.Clear();
sp.Children.Add(new TextBlock
{
Text = $"Error: {ex.Message}",
Foreground = System.Windows.Media.Brushes.Red,
FontSize = 12,
TextWrapping = TextWrapping.Wrap
});
}
}
private StackPanel CreateControlSimple(
XMonitor monitorModel,
int? brightness,
int? contrast,
int? currentInputSource,
List<InputSourceOption> inputOptions,
string powerStatus)
{
var container = new StackPanel
{
Orientation = Orientation.Vertical,
Margin = new Thickness(0, 5, 0, 10)
};
// Monitor name
container.Children.Add(new TextBlock
{
Text = $"{monitorModel.MonitorName} ({monitorModel.SerialNumber})",
Foreground = System.Windows.Media.Brushes.White,
FontSize = 14,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 5)
});
// Brightness
var brightnessPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
brightnessPanel.Children.Add(new TextBlock { Text = "Brightness:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
var brightnessSlider = new Slider { Minimum = 0, Maximum = 100, Value = brightness ?? 50, Width = 150, Tag = monitorModel.SerialNumber };
var brightnessValue = new TextBlock { Text = (brightness ?? 50).ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) };
brightnessSlider.ValueChanged += (s, e) => brightnessValue.Text = ((int)e.NewValue).ToString();
brightnessSlider.PreviewMouseUp += async (s, e) =>
{
var sld = s as Slider;
if (sld?.Tag is string sn)
await CMMCommand.SetBrightness(sn, (int)sld.Value);
};
brightnessPanel.Children.Add(brightnessSlider);
brightnessPanel.Children.Add(brightnessValue);
container.Children.Add(brightnessPanel);
// Contrast
var contrastPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
contrastPanel.Children.Add(new TextBlock { Text = "Contrast:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
var contrastSlider = new Slider { Minimum = 0, Maximum = 100, Value = contrast ?? 50, Width = 150, Tag = monitorModel.SerialNumber };
var contrastValue = new TextBlock { Text = (contrast ?? 50).ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) };
contrastSlider.ValueChanged += (s, e) => contrastValue.Text = ((int)e.NewValue).ToString();
contrastSlider.PreviewMouseUp += async (s, e) =>
{
var sld = s as Slider;
if (sld?.Tag is string sn)
await CMMCommand.SetContrast(sn, (int)sld.Value);
};
contrastPanel.Children.Add(contrastSlider);
contrastPanel.Children.Add(contrastValue);
container.Children.Add(contrastPanel);
// Input source (if available)
if (inputOptions.Count > 0)
{
var inputPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
inputPanel.Children.Add(new TextBlock { Text = "Input:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
var combo = new ComboBox { Width = 150, ItemsSource = inputOptions, DisplayMemberPath = "Name", Tag = monitorModel.SerialNumber };
if (currentInputSource.HasValue)
combo.SelectedItem = inputOptions.Find(o => o.Value == currentInputSource.Value);
combo.SelectionChanged += async (s, e) =>
{
var cb = s as ComboBox;
if (cb?.Tag is string sn && cb.SelectedItem is InputSourceOption opt)
await CMMCommand.SetInputSource(sn, opt.Value);
};
inputPanel.Children.Add(combo);
container.Children.Add(inputPanel);
}
// Power
var powerPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) };
powerPanel.Children.Add(new TextBlock { Text = "Power:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 });
var powerBtn = new Button { Content = powerStatus, Width = 150, Tag = monitorModel.SerialNumber };
powerBtn.Click += async (s, e) =>
{
var btn = s as Button;
if (btn?.Tag is string sn)
{
var status = await CMMCommand.GetMonPowerStatus(sn);
if (status == "Sleep" || status == "PowerOff")
await CMMCommand.PowerOn(sn);
else
await CMMCommand.Sleep(sn);
await Task.Delay(1000);
btn.Content = await CMMCommand.GetMonPowerStatus(sn);
}
};
powerPanel.Children.Add(powerBtn);
container.Children.Add(powerPanel);
return container;
}
private StackPanel CreateControl(
XMonitor monitorModel,
int? brightness,
int? contrast,
int? currentInputSource,
List<InputSourceOption> inputOptions,
string powerStatus)
{
var container = new StackPanel
{
Orientation = Orientation.Vertical,
Margin = new Thickness(10, 5, 10, 10)
};
// Monitor name header
var header = new TextBlock
{
Text = $"{monitorModel.MonitorName} ({monitorModel.SerialNumber})",
HorizontalAlignment = HorizontalAlignment.Left,
Style = (Style)FindResource("LableStyle"),
Margin = new Thickness(0, 0, 0, 5)
};
container.Children.Add(header);
// Brightness slider
var brightnessPanel = CreateSliderControl(
Lang.Find("Brightness"),
brightness ?? 50,
monitorModel.SerialNumber,
async (sn, value) => await CMMCommand.SetBrightness(sn, value));
container.Children.Add(brightnessPanel);
// Contrast slider
var contrastPanel = CreateSliderControl(
Lang.Find("Contrast"),
contrast ?? 50,
monitorModel.SerialNumber,
async (sn, value) => await CMMCommand.SetContrast(sn, value));
container.Children.Add(contrastPanel);
// Input source dropdown
if (inputOptions.Count > 0)
{
var inputPanel = CreateInputSourceControl(
monitorModel.SerialNumber,
currentInputSource,
inputOptions);
container.Children.Add(inputPanel);
}
// Power button
var powerPanel = CreatePowerControl(monitorModel.SerialNumber, powerStatus);
container.Children.Add(powerPanel);
return container;
}
private StackPanel CreateSliderControl(
string label,
int currentValue,
string monitorSN,
Func<string, int, Task> onValueChanged)
{
var panel = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 3, 0, 3)
};
var labelBlock = new TextBlock
{
Text = label,
Width = 80,
VerticalAlignment = VerticalAlignment.Center,
Style = (Style)FindResource("LableStyle")
};
var slider = new Slider
{
Style = (Style)FindResource("Horizontal_Slider"),
Minimum = 0,
Maximum = 100,
Value = currentValue,
Tag = monitorSN,
Width = 180
};
var valueBlock = new TextBlock
{
Text = currentValue.ToString(),
Width = 35,
VerticalAlignment = VerticalAlignment.Center,
Style = (Style)FindResource("LableStyle"),
TextAlignment = TextAlignment.Right
};
// Update value display while dragging
slider.ValueChanged += (s, e) =>
{
valueBlock.Text = ((int)e.NewValue).ToString();
};
// Only send command on release (PreviewMouseUp)
slider.PreviewMouseUp += async (s, e) =>
{
var sld = s as Slider;
var sn = sld?.Tag?.ToString();
if (!string.IsNullOrEmpty(sn) && sld != null)
{
await onValueChanged(sn, (int)sld.Value);
}
};
panel.Children.Add(labelBlock);
panel.Children.Add(slider);
panel.Children.Add(valueBlock);
return panel;
}
private StackPanel CreateInputSourceControl(
string monitorSN,
int? currentInputSource,
List<InputSourceOption> options)
{
var panel = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 3, 0, 3)
};
var labelBlock = new TextBlock
{
Text = Lang.Find("InputSource"),
Width = 80,
VerticalAlignment = VerticalAlignment.Center,
Style = (Style)FindResource("LableStyle")
};
var comboBox = new ComboBox
{
Width = 180,
Tag = monitorSN,
ItemsSource = options,
DisplayMemberPath = "Name"
};
// Set current selection
if (currentInputSource.HasValue)
{
var currentOption = options.Find(o => o.Value == currentInputSource.Value);
if (currentOption != null)
{
comboBox.SelectedItem = currentOption;
}
}
comboBox.SelectionChanged += async (s, e) =>
{
var cb = s as ComboBox;
var sn = cb?.Tag?.ToString();
var selectedOption = cb?.SelectedItem as InputSourceOption;
if (!string.IsNullOrEmpty(sn) && selectedOption != null)
{
await CMMCommand.SetInputSource(sn, selectedOption.Value);
}
};
panel.Children.Add(labelBlock);
panel.Children.Add(comboBox);
return panel;
}
private StackPanel CreatePowerControl(string monitorSN, string powerStatus)
{
var panel = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 5, 0, 0)
};
var labelBlock = new TextBlock
{
Text = Lang.Find("Power"),
Width = 80,
VerticalAlignment = VerticalAlignment.Center,
Style = (Style)FindResource("LableStyle")
};
var btn = new Button
{
Tag = monitorSN,
Content = GetLocalizedPowerStatus(powerStatus),
Style = (Style)FindResource("TextButtonStyle"),
Width = 180
};
btn.Click += async (s, e) => await TogglePower(s, e);
panel.Children.Add(labelBlock);
panel.Children.Add(btn);
return panel;
}
private string GetLocalizedPowerStatus(string status)
{
return status switch
{
"PowerOn" => Lang.Find("PowerOn"),
"Sleep" => Lang.Find("Sleep"),
"PowerOff" => Lang.Find("PowerOff"),
_ => status
};
}
private async Task TogglePower(object sender, RoutedEventArgs e)
{
var btn = sender as Button;
var tag = btn?.Tag?.ToString();
if (string.IsNullOrEmpty(tag)) return;
var currentStatus = await CMMCommand.GetMonPowerStatus(tag);
if (currentStatus == "Sleep" || currentStatus == "PowerOff")
{
await CMMCommand.PowerOn(tag);
}
else
{
await CMMCommand.Sleep(tag);
}
await Task.Delay(1000);
var newStatus = await CMMCommand.GetMonPowerStatus(tag);
btn!.Content = GetLocalizedPowerStatus(newStatus);
}
private void Border_MouseLeave(object sender, MouseEventArgs e)
{
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
// Use Dispatcher to let the popup fully render first
Dispatcher.BeginInvoke(new Action(async () =>
{
try
{
await Refresh();
}
catch (Exception ex)
{
sp.Children.Clear();
sp.Children.Add(new TextBlock
{
Text = $"Error: {ex.Message}",
Foreground = System.Windows.Media.Brushes.Red,
FontSize = 12,
TextWrapping = TextWrapping.Wrap
});
}
}), System.Windows.Threading.DispatcherPriority.Background);
}
}