This commit is contained in:
@@ -8,16 +8,13 @@ using System.Windows.Threading;
|
|||||||
namespace PLCDataMonitor
|
namespace PLCDataMonitor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自定义滚动报警控件(跑马灯效果 + 闪烁)
|
/// 滚动报警控件(跑马灯效果)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AlarmScrollingText : ContentControl
|
public class AlarmScrollingText : ContentControl
|
||||||
{
|
{
|
||||||
private TextBlock _textBlock;
|
private TextBlock _textBlock;
|
||||||
private Border _clipBorder;
|
private Border _clipBorder;
|
||||||
private Storyboard _scrollStoryboard;
|
private Storyboard _scrollStoryboard;
|
||||||
private Storyboard _blinkStoryboard;
|
|
||||||
private double _currentOffset = 0;
|
|
||||||
private DispatcherTimer _resetTimer;
|
|
||||||
|
|
||||||
public static readonly DependencyProperty TextProperty =
|
public static readonly DependencyProperty TextProperty =
|
||||||
DependencyProperty.Register("Text", typeof(string), typeof(AlarmScrollingText),
|
DependencyProperty.Register("Text", typeof(string), typeof(AlarmScrollingText),
|
||||||
@@ -38,127 +35,144 @@ namespace PLCDataMonitor
|
|||||||
|
|
||||||
public AlarmScrollingText()
|
public AlarmScrollingText()
|
||||||
{
|
{
|
||||||
// 默认样式
|
|
||||||
DefaultStyleKey = typeof(AlarmScrollingText);
|
DefaultStyleKey = typeof(AlarmScrollingText);
|
||||||
|
|
||||||
|
// 添加一个简单的默认样式,以防找不到模板
|
||||||
|
this.Loaded += OnLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnApplyTemplate()
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate();
|
// 确保获取到模板中的元素
|
||||||
|
ApplyTemplate();
|
||||||
|
|
||||||
_textBlock = GetTemplateChild("PART_TextBlock") as TextBlock;
|
_textBlock = GetTemplateChild("PART_TextBlock") as TextBlock;
|
||||||
_clipBorder = GetTemplateChild("PART_ClipBorder") as Border;
|
_clipBorder = GetTemplateChild("PART_ClipBorder") as Border;
|
||||||
|
|
||||||
if (_textBlock != null && _clipBorder != null)
|
if (_textBlock != null && _clipBorder != null)
|
||||||
{
|
{
|
||||||
_textBlock.Loaded += OnTextBlockLoaded;
|
_textBlock.SizeChanged += (s, args) => CheckAndStartScrolling();
|
||||||
_textBlock.SizeChanged += OnTextBlockSizeChanged;
|
_clipBorder.SizeChanged += (s, args) => CheckAndStartScrolling();
|
||||||
InitializeAnimations();
|
|
||||||
|
// 初始化滚动动画
|
||||||
|
InitializeScrollAnimation();
|
||||||
|
|
||||||
|
// 检查是否需要滚动
|
||||||
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(CheckAndStartScrolling));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 如果找不到模板元素,添加一个简单的 TextBlock 作为后备
|
||||||
|
CreateFallbackUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTextBlockLoaded(object sender, RoutedEventArgs e)
|
private void CreateFallbackUI()
|
||||||
{
|
{
|
||||||
StartScrollingIfNeeded();
|
var border = new Border
|
||||||
|
{
|
||||||
|
Background = this.Background,
|
||||||
|
CornerRadius = new CornerRadius(4),
|
||||||
|
ClipToBounds = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_textBlock = new TextBlock
|
||||||
|
{
|
||||||
|
Text = this.Text,
|
||||||
|
FontSize = 14,
|
||||||
|
FontWeight = FontWeights.Bold,
|
||||||
|
Foreground = Brushes.Red,
|
||||||
|
TextWrapping = TextWrapping.NoWrap,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
|
||||||
|
border.Child = _textBlock;
|
||||||
|
this.Content = border;
|
||||||
|
_clipBorder = border;
|
||||||
|
|
||||||
|
_textBlock.SizeChanged += (s, args) => CheckAndStartScrolling();
|
||||||
|
_clipBorder.SizeChanged += (s, args) => CheckAndStartScrolling();
|
||||||
|
|
||||||
|
InitializeScrollAnimation();
|
||||||
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(CheckAndStartScrolling));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTextBlockSizeChanged(object sender, SizeChangedEventArgs e)
|
private void InitializeScrollAnimation()
|
||||||
{
|
{
|
||||||
StartScrollingIfNeeded();
|
if (_textBlock == null) return;
|
||||||
|
|
||||||
|
_scrollStoryboard = new Storyboard();
|
||||||
|
var doubleAnimation = new DoubleAnimation
|
||||||
|
{
|
||||||
|
From = 0,
|
||||||
|
To = -500, // 初始值,稍后在 CheckAndStartScrolling 中动态更新
|
||||||
|
Duration = TimeSpan.FromSeconds(5),
|
||||||
|
RepeatBehavior = RepeatBehavior.Forever
|
||||||
|
};
|
||||||
|
Storyboard.SetTarget(doubleAnimation, _textBlock);
|
||||||
|
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
|
||||||
|
_scrollStoryboard.Children.Add(doubleAnimation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void OnTextChanged(string newText)
|
private void OnTextChanged(string newText)
|
||||||
{
|
{
|
||||||
if (_textBlock != null)
|
if (_textBlock != null)
|
||||||
{
|
{
|
||||||
_textBlock.Text = newText;
|
_textBlock.Text = newText;
|
||||||
StartScrollingIfNeeded();
|
// 延迟一下,等待布局更新后再检查
|
||||||
StartBlinking(newText);
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(CheckAndStartScrolling));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeAnimations()
|
private void CheckAndStartScrolling()
|
||||||
{
|
{
|
||||||
// 滚动动画
|
if (_textBlock == null || _clipBorder == null || _scrollStoryboard == null)
|
||||||
_scrollStoryboard = new Storyboard();
|
return;
|
||||||
var doubleAnimation = new DoubleAnimation
|
|
||||||
{
|
|
||||||
From = 0,
|
|
||||||
To = -500, // 临时值,后面动态计算
|
|
||||||
Duration = TimeSpan.FromSeconds(8),
|
|
||||||
RepeatBehavior = RepeatBehavior.Forever
|
|
||||||
};
|
|
||||||
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
|
|
||||||
_scrollStoryboard.Children.Add(doubleAnimation);
|
|
||||||
|
|
||||||
// 闪烁动画(前景色交替变化)
|
|
||||||
_blinkStoryboard = new Storyboard { RepeatBehavior = RepeatBehavior.Forever };
|
|
||||||
var colorAnimation = new ColorAnimation
|
|
||||||
{
|
|
||||||
From = Colors.Red,
|
|
||||||
To = Colors.Orange,
|
|
||||||
Duration = TimeSpan.FromSeconds(0.5),
|
|
||||||
AutoReverse = true,
|
|
||||||
RepeatBehavior = RepeatBehavior.Forever
|
|
||||||
};
|
|
||||||
Storyboard.SetTargetProperty(colorAnimation, new PropertyPath("Foreground.Color"));
|
|
||||||
_blinkStoryboard.Children.Add(colorAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartScrollingIfNeeded()
|
|
||||||
{
|
|
||||||
if (_textBlock == null || _clipBorder == null) return;
|
|
||||||
|
|
||||||
// 停止当前动画
|
// 停止当前动画
|
||||||
_scrollStoryboard?.Stop(_textBlock);
|
_scrollStoryboard.Stop(_textBlock);
|
||||||
_textBlock.RenderTransform = new TranslateTransform();
|
_textBlock.RenderTransform = null;
|
||||||
|
|
||||||
// 如果没有报警或报警文本为空,不启动滚动
|
// 如果没有报警或报警文本为"无报警",不启动滚动
|
||||||
if (string.IsNullOrWhiteSpace(Text) || Text == "无报警")
|
if (string.IsNullOrWhiteSpace(Text) || Text == "无报警")
|
||||||
return;
|
|
||||||
|
|
||||||
// 等待布局完成后获取实际宽度
|
|
||||||
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
|
|
||||||
{
|
{
|
||||||
double textWidth = _textBlock.ActualWidth;
|
|
||||||
double containerWidth = _clipBorder.ActualWidth;
|
|
||||||
|
|
||||||
if (textWidth <= containerWidth)
|
|
||||||
{
|
|
||||||
// 文本不够长,不需要滚动,但保持静止
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新设置动画的目标偏移量
|
|
||||||
double targetOffset = containerWidth - textWidth;
|
|
||||||
var animation = _scrollStoryboard.Children[0] as DoubleAnimation;
|
|
||||||
if (animation != null)
|
|
||||||
{
|
|
||||||
animation.From = 0;
|
|
||||||
animation.To = targetOffset;
|
|
||||||
animation.Duration = TimeSpan.FromSeconds(Math.Max(4, textWidth / 50.0)); // 速度自适应
|
|
||||||
}
|
|
||||||
|
|
||||||
_textBlock.RenderTransform = new TranslateTransform();
|
|
||||||
_scrollStoryboard.Begin(_textBlock, true);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartBlinking(string alarmText)
|
|
||||||
{
|
|
||||||
if (_textBlock == null) return;
|
|
||||||
|
|
||||||
// 停止当前闪烁
|
|
||||||
_blinkStoryboard?.Stop(_textBlock);
|
|
||||||
|
|
||||||
// 如果是无效报警或无报警,恢复正常颜色并停止闪烁
|
|
||||||
if (string.IsNullOrWhiteSpace(alarmText) || alarmText == "无报警")
|
|
||||||
{
|
|
||||||
_textBlock.Foreground = SystemColors.ControlTextBrush;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始闪烁动画
|
// 强制更新布局以获取准确宽度
|
||||||
_blinkStoryboard.Begin(_textBlock, true);
|
_textBlock.UpdateLayout();
|
||||||
|
_clipBorder.UpdateLayout();
|
||||||
|
|
||||||
|
double textWidth = _textBlock.ActualWidth;
|
||||||
|
double containerWidth = _clipBorder.ActualWidth;
|
||||||
|
|
||||||
|
// 如果文本宽度小于等于容器宽度,不需要滚动
|
||||||
|
if (textWidth <= containerWidth)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算滚动目标位置
|
||||||
|
double targetOffset = -textWidth; // 滚动到文本的左边界
|
||||||
|
|
||||||
|
// 更新动画参数
|
||||||
|
var animation = _scrollStoryboard.Children[0] as DoubleAnimation;
|
||||||
|
if (animation != null)
|
||||||
|
{
|
||||||
|
animation.From = containerWidth; // 从容器右边开始
|
||||||
|
animation.To = targetOffset; // 滚动到文本的左边界
|
||||||
|
// 根据文本长度计算滚动速度
|
||||||
|
double speed = Math.Max(3, Math.Abs(textWidth + containerWidth) / 50.0);
|
||||||
|
animation.Duration = TimeSpan.FromSeconds(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置初始变换
|
||||||
|
_textBlock.RenderTransform = new TranslateTransform();
|
||||||
|
|
||||||
|
// 开始滚动动画
|
||||||
|
_scrollStoryboard.Begin(_textBlock, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
30
App.xaml
30
App.xaml
@@ -8,6 +8,36 @@
|
|||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary Source="Themes/Generic.xaml" />
|
<ResourceDictionary Source="Themes/Generic.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
|
||||||
|
<Style TargetType="local:AlarmScrollingText">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="local:AlarmScrollingText">
|
||||||
|
<Border x:Name="PART_ClipBorder"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="4"
|
||||||
|
ClipToBounds="True"
|
||||||
|
Padding="5,5">
|
||||||
|
<TextBlock x:Name="PART_TextBlock"
|
||||||
|
Text="{TemplateBinding Text}"
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="Red"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -152,23 +152,23 @@ namespace PLCDataMonitor
|
|||||||
double totalSeconds = Math.Max(1, (lastTime - firstTime).TotalSeconds);
|
double totalSeconds = Math.Max(1, (lastTime - firstTime).TotalSeconds);
|
||||||
|
|
||||||
// 清理超出时间范围的数据
|
// 清理超出时间范围的数据
|
||||||
if (totalSeconds > _currentTimeRange)
|
if (totalSeconds > _currentTimeRange)
|
||||||
|
{
|
||||||
|
var maxDataCount = (int)(_currentTimeRange * 10);
|
||||||
|
// 使用 TryDequeue 安全地移除多余项
|
||||||
|
while (_sharedDataQueue.Count > maxDataCount)
|
||||||
{
|
{
|
||||||
var maxDataCount = (int)(_currentTimeRange * 10);
|
_sharedDataQueue.TryDequeue(out _);
|
||||||
// 使用 TryDequeue 安全地移除多余项
|
|
||||||
while (_sharedDataQueue.Count > maxDataCount)
|
|
||||||
{
|
|
||||||
_sharedDataQueue.TryDequeue(out _);
|
|
||||||
}
|
|
||||||
allData = _sharedDataQueue.ToList();
|
|
||||||
|
|
||||||
if (allData.Count > 0)
|
|
||||||
{
|
|
||||||
firstTime = allData.First().Item1;
|
|
||||||
lastTime = allData.Last().Item1;
|
|
||||||
totalSeconds = Math.Max(1, (lastTime - firstTime).TotalSeconds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
allData = _sharedDataQueue.ToList();
|
||||||
|
|
||||||
|
if (allData.Count > 0)
|
||||||
|
{
|
||||||
|
firstTime = allData.First().Item1;
|
||||||
|
lastTime = allData.Last().Item1;
|
||||||
|
totalSeconds = Math.Max(1, (lastTime - firstTime).TotalSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算Y轴范围
|
// 计算Y轴范围
|
||||||
double minValue = double.MaxValue;
|
double minValue = double.MaxValue;
|
||||||
@@ -455,6 +455,10 @@ namespace PLCDataMonitor
|
|||||||
Friction2Polyline.Points = points2;
|
Friction2Polyline.Points = points2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private double CalculateNiceInterval(double range)
|
private double CalculateNiceInterval(double range)
|
||||||
{
|
{
|
||||||
if (range <= 0) return 10;
|
if (range <= 0) return 10;
|
||||||
|
|||||||
@@ -26,17 +26,20 @@
|
|||||||
FontSize="12" Foreground="#333" Margin="0,0,15,0"/>
|
FontSize="12" Foreground="#333" Margin="0,0,15,0"/>
|
||||||
<Button x:Name="PauseButton" Content="暂停" Width="70" Height="28" Margin="5,0"
|
<Button x:Name="PauseButton" Content="暂停" Width="70" Height="28" Margin="5,0"
|
||||||
Background="#E67E22" Foreground="White" BorderThickness="0" Click="PauseButton_Click"/>
|
Background="#E67E22" Foreground="White" BorderThickness="0" Click="PauseButton_Click"/>
|
||||||
<Button x:Name="ResumeButton" Content="继续" Width="70" Height="28" Margin="5,0"
|
<!--<Button x:Name="ResumeButton" Content="继续" Width="70" Height="28" Margin="5,0"
|
||||||
Background="#27AE60" Foreground="White" BorderThickness="0" Click="ResumeButton_Click"/>
|
Background="#27AE60" Foreground="White" BorderThickness="0" Click="ResumeButton_Click"/>-->
|
||||||
|
|
||||||
|
|
||||||
<!-- 在 ResumeButton 后添加 -->
|
<!-- 在 ResumeButton 后添加 -->
|
||||||
|
|
||||||
<local:AlarmScrollingText x:Name="AlarmScrollingCtrl"
|
<local:AlarmScrollingText x:Name="AlarmScrollingCtrl"
|
||||||
Text="无报警"
|
Text="无报警"
|
||||||
Background="#FFF0F0F0"
|
Background="#FFF0F0F0"
|
||||||
Height="40"
|
Height="40"
|
||||||
Margin="5"/>
|
Width="400"
|
||||||
|
Margin="5"
|
||||||
|
HorizontalAlignment="Center"/>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace PLCDataMonitor
|
namespace PLCDataMonitor
|
||||||
{
|
{
|
||||||
@@ -19,21 +20,66 @@ namespace PLCDataMonitor
|
|||||||
public event EventHandler PauseRequested;
|
public event EventHandler PauseRequested;
|
||||||
public event EventHandler ResumeRequested;
|
public event EventHandler ResumeRequested;
|
||||||
|
|
||||||
|
// 【修改点 2】合并点击逻辑
|
||||||
private void PauseButton_Click(object sender, RoutedEventArgs e)
|
private void PauseButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
PauseRequested?.Invoke(this, EventArgs.Empty);
|
string currentContent = PauseButton.Content.ToString();
|
||||||
|
|
||||||
|
if (currentContent == "暂停")
|
||||||
|
{
|
||||||
|
// 当前是运行状态,点击后变为暂停
|
||||||
|
PauseRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
// 立即更新按钮样式为“继续”
|
||||||
|
PauseButton.Content = "继续";
|
||||||
|
PauseButton.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27AE60")); // 绿色
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 当前是暂停状态,点击后变为继续
|
||||||
|
ResumeRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
// 立即更新按钮样式为“暂停”
|
||||||
|
PauseButton.Content = "暂停";
|
||||||
|
PauseButton.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E67E22")); // 橙色
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResumeButton_Click(object sender, RoutedEventArgs e)
|
//private void ResumeButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
//{
|
||||||
|
// ResumeRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 【新增】供 MainWindow 调用的方法,以防 PLC 状态自行变化导致按钮状态不同步
|
||||||
|
public void SetButtonState(bool isPaused)
|
||||||
{
|
{
|
||||||
ResumeRequested?.Invoke(this, EventArgs.Empty);
|
if (isPaused)
|
||||||
|
{
|
||||||
|
PauseButton.Content = "继续";
|
||||||
|
PauseButton.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27AE60"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PauseButton.Content = "暂停";
|
||||||
|
PauseButton.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E67E22"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void UpdateAlarms(string alarmMessage)
|
public void UpdateAlarms(string alarmMessage)
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() => // 更新滚动报警控件的文本
|
||||||
|
AlarmScrollingCtrl.Text = alarmMessage);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
Dispatcher.Invoke(() => // 更新滚动报警控件的文本
|
}
|
||||||
AlarmScrollingCtrl.Text = alarmMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新数据显示
|
// 更新数据显示
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ namespace PLCDataMonitor
|
|||||||
private IModbusMaster _modbusMaster;
|
private IModbusMaster _modbusMaster;
|
||||||
|
|
||||||
|
|
||||||
// PLC 地址(根据文档修正)
|
// 暂停控制(HR500.04)
|
||||||
private const int PauseControlCoil = 25053; // 写入 true=暂停,false=继续(线圈地址)
|
private const int PauseControlCoil = 41204; // 写入 true=暂停,false=继续
|
||||||
private const int PauseStatusCoil = 25053; // 读取当前暂停状态(同一地址)
|
private const int PauseStatusCoil = 41204; // 读取当前暂停状态(同一地址)
|
||||||
|
|
||||||
|
|
||||||
// 记录暂停时间段(用于导出过滤)
|
// 记录暂停时间段(用于导出过滤)
|
||||||
@@ -55,14 +55,6 @@ namespace PLCDataMonitor
|
|||||||
|
|
||||||
public List<(DateTime start, DateTime end)> PausePeriods => _pausePeriods;
|
public List<(DateTime start, DateTime end)> PausePeriods => _pausePeriods;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 报警线圈地址(连续读取)
|
|
||||||
private const int AlarmStartAddr_WR4 = 25065; // WR4.0
|
|
||||||
private const int AlarmCount_WR4 = 5; // WR4.0 ~ WR4.4
|
|
||||||
private const int AlarmStartAddr_WR9 = 25145; // WR9.0
|
|
||||||
private const int AlarmCount_WR9 = 5; // WR9.0 ~ WR9.4
|
|
||||||
|
|
||||||
// 添加清空方法
|
// 添加清空方法
|
||||||
public void ClearPausePeriods()
|
public void ClearPausePeriods()
|
||||||
{
|
{
|
||||||
@@ -268,8 +260,6 @@ namespace PLCDataMonitor
|
|||||||
|
|
||||||
private void ReadDataLoop()
|
private void ReadDataLoop()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
//int i = 0;
|
//int i = 0;
|
||||||
//while (i < 100)
|
//while (i < 100)
|
||||||
//{
|
//{
|
||||||
@@ -317,11 +307,6 @@ namespace PLCDataMonitor
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
while (_keepReading)
|
while (_keepReading)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -359,6 +344,15 @@ namespace PLCDataMonitor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_lastPausedState = isPaused;
|
_lastPausedState = isPaused;
|
||||||
|
|
||||||
|
// 【新增】同步更新 HomePage 上的按钮状态
|
||||||
|
Dispatcher.BeginInvoke(new Action(() =>
|
||||||
|
{
|
||||||
|
_homePage.SetButtonState(isPaused);
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
// --- 结束记录暂停时间段 ---
|
// --- 结束记录暂停时间段 ---
|
||||||
|
|
||||||
@@ -371,31 +365,38 @@ namespace PLCDataMonitor
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 读取报警状态
|
// ----- 读取报警状态(HR504 / HR509)-----
|
||||||
bool[] alarmsWR4 = null;
|
bool[] alarmsHR504 = null;
|
||||||
bool[] alarmsWR9 = null;
|
bool[] alarmsHR509 = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
alarmsWR4 = _modbusMaster?.ReadCoils(0x01, (ushort)AlarmStartAddr_WR4, AlarmCount_WR4);
|
// 连续读取 HR504.1 ~ HR504.5(5个线圈,起始 41265)
|
||||||
alarmsWR9 = _modbusMaster?.ReadCoils(0x01, (ushort)AlarmStartAddr_WR9, AlarmCount_WR9);
|
alarmsHR504 = _modbusMaster?.ReadCoils(0x01, 41265, 5);
|
||||||
|
// 连续读取 HR509.0 ~ HR509.3(4个线圈,起始 41345)
|
||||||
|
alarmsHR509 = _modbusMaster?.ReadCoils(0x01, 41344, 4);
|
||||||
}
|
}
|
||||||
catch { } // 读取失败时不更新报警
|
catch { }
|
||||||
|
|
||||||
// 构建报警消息
|
|
||||||
string alarmMessage = "";
|
string alarmMessage = "";
|
||||||
if (alarmsWR4 != null && alarmsWR4.Length >= 5)
|
if (alarmsHR504 != null && alarmsHR504.Length >= 5)
|
||||||
{
|
{
|
||||||
if (alarmsWR4[0]) alarmMessage += "超温95℃ ";
|
// 根据实测:索引0=41265=已达上限水位
|
||||||
if (alarmsWR4[1]) alarmMessage += "超上限水位 ";
|
if (alarmsHR504[0]) alarmMessage += "已达上限水位 ";
|
||||||
if (alarmsWR4[2]) alarmMessage += "急停被按下 ";
|
// 索引1=41266=急停被按下
|
||||||
if (alarmsWR4[3]) alarmMessage += "测试完成 ";
|
if (alarmsHR504[1]) alarmMessage += "急停被按下 ";
|
||||||
if (alarmsWR4[4]) alarmMessage += "摩擦过大 ";
|
// 索引2=41267=测试完成(待确认)
|
||||||
|
if (alarmsHR504[2]) alarmMessage += "测试完成 ";
|
||||||
|
// 索引3=41268=摩擦力过大(待确认)
|
||||||
|
if (alarmsHR504[3]) alarmMessage += "摩擦力过大 ";
|
||||||
|
// 索引4=41269=超温95℃(待确认)
|
||||||
|
if (alarmsHR504[4]) alarmMessage += "超温95℃ ";
|
||||||
}
|
}
|
||||||
if (alarmsWR9 != null && alarmsWR9.Length >= 5)
|
if (alarmsHR509 != null && alarmsHR509.Length >= 4)
|
||||||
{
|
{
|
||||||
if (alarmsWR9[0]) alarmMessage += "1工位低位水 ";
|
if (alarmsHR509[0]) alarmMessage += "1工位低水位 ";
|
||||||
if (alarmsWR9[2]) alarmMessage += "水泵未运行 "; // 索引 2 对应 WR9.2
|
if (alarmsHR509[2]) alarmMessage += "水泵未运行 ";
|
||||||
if (alarmsWR9[3]) alarmMessage += "2工位低位水 ";
|
// 索引3=41348=2工位低水位
|
||||||
|
if (alarmsHR509[3]) alarmMessage += "2工位低水位 ";
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(alarmMessage))
|
if (string.IsNullOrEmpty(alarmMessage))
|
||||||
alarmMessage = "无报警";
|
alarmMessage = "无报警";
|
||||||
@@ -403,7 +404,6 @@ namespace PLCDataMonitor
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 更新UI(数据监控页+曲线页)
|
// 更新UI(数据监控页+曲线页)
|
||||||
// 使用 BeginInvoke 避免阻塞后台读取线程,减少 UI 卡死风险
|
// 使用 BeginInvoke 避免阻塞后台读取线程,减少 UI 卡死风险
|
||||||
Dispatcher.BeginInvoke(new Action(() =>
|
Dispatcher.BeginInvoke(new Action(() =>
|
||||||
@@ -424,52 +424,56 @@ namespace PLCDataMonitor
|
|||||||
// 新增:更新报警显示
|
// 新增:更新报警显示
|
||||||
_homePage.UpdateAlarms(alarmMessage);
|
_homePage.UpdateAlarms(alarmMessage);
|
||||||
|
|
||||||
// 关键修改:确保曲线数据正确记录
|
|
||||||
if (data.setCount > 0 && data.actualCount > 0 &&
|
if (!isPaused)
|
||||||
data.setCount > data.actualCount) // 点击了开始,且实际次数小于设定次数,才更新曲线
|
|
||||||
{
|
{
|
||||||
// 创建数据点
|
// 关键修改:确保曲线数据正确记录
|
||||||
var dataPoint = new Tuple<DateTime, double, double>(
|
if (data.setCount > 0 && data.actualCount > 0 &&
|
||||||
DateTime.Now,
|
data.setCount > data.actualCount) // 点击了开始,且实际次数小于设定次数,才更新曲线
|
||||||
friction1,
|
{
|
||||||
friction2
|
// 创建数据点
|
||||||
);
|
var dataPoint = new Tuple<DateTime, double, double>(
|
||||||
|
DateTime.Now,
|
||||||
|
friction1,
|
||||||
|
friction2
|
||||||
|
);
|
||||||
|
|
||||||
// 添加到共享队列
|
// 添加到共享队列
|
||||||
_sharedCurveDataQueue.Enqueue(dataPoint);
|
_sharedCurveDataQueue.Enqueue(dataPoint);
|
||||||
|
|
||||||
// 通知曲线页面更新
|
// 通知曲线页面更新
|
||||||
_curvePage.AddFrictionData(friction1, friction2);
|
_curvePage.AddFrictionData(friction1, friction2);
|
||||||
|
|
||||||
// 关键修改:只添加一次,不需要重复添加到报表页面
|
|
||||||
// 报表页面的CurveDataQueue和_sharedCurveDataQueue是同一个对象
|
}
|
||||||
|
|
||||||
|
if (data.setCount > 0 && data.actualCount > 0 &&
|
||||||
|
data.setCount == data.actualCount && !_hasInsertedReport) // 一次测试执行完成,保存报表
|
||||||
|
{
|
||||||
|
// 创建报表记录(时间+压力+最大摩擦力)
|
||||||
|
var reportRecord = new ReportData(
|
||||||
|
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), // 当前时间
|
||||||
|
pressure1.ToString("F1"), // 工位1压力
|
||||||
|
pressure2.ToString("F1"), // 工位2压力
|
||||||
|
maxFriction1.ToString("F1"), // 工位1最大摩擦力
|
||||||
|
maxFriction2.ToString("F1"),
|
||||||
|
data.t1,
|
||||||
|
data.t2,
|
||||||
|
data.D
|
||||||
|
);
|
||||||
|
|
||||||
|
// 插入报表
|
||||||
|
_reportPage.AddReportRecord(reportRecord);
|
||||||
|
// 标记为已插入(避免同一批次重复插入)
|
||||||
|
_hasInsertedReport = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.actualCount < data.setCount)
|
||||||
|
{
|
||||||
|
_hasInsertedReport = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.setCount > 0 && data.actualCount > 0 &&
|
|
||||||
data.setCount == data.actualCount && !_hasInsertedReport) // 一次测试执行完成,保存报表
|
|
||||||
{
|
|
||||||
// 创建报表记录(时间+压力+最大摩擦力)
|
|
||||||
var reportRecord = new ReportData(
|
|
||||||
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), // 当前时间
|
|
||||||
pressure1.ToString("F1"), // 工位1压力
|
|
||||||
pressure2.ToString("F1"), // 工位2压力
|
|
||||||
maxFriction1.ToString("F1"), // 工位1最大摩擦力
|
|
||||||
maxFriction2.ToString("F1"),
|
|
||||||
data.t1,
|
|
||||||
data.t2,
|
|
||||||
data.D
|
|
||||||
);
|
|
||||||
|
|
||||||
// 插入报表
|
|
||||||
_reportPage.AddReportRecord(reportRecord);
|
|
||||||
// 标记为已插入(避免同一批次重复插入)
|
|
||||||
_hasInsertedReport = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.actualCount < data.setCount)
|
|
||||||
{
|
|
||||||
_hasInsertedReport = false;
|
|
||||||
}
|
|
||||||
}), System.Windows.Threading.DispatcherPriority.Background);
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,6 +598,7 @@ namespace PLCDataMonitor
|
|||||||
if (_isConnected && _modbusMaster != null)
|
if (_isConnected && _modbusMaster != null)
|
||||||
{
|
{
|
||||||
_modbusMaster.WriteSingleCoil(0x01, (ushort)PauseControlCoil, true);
|
_modbusMaster.WriteSingleCoil(0x01, (ushort)PauseControlCoil, true);
|
||||||
|
Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -609,6 +614,7 @@ namespace PLCDataMonitor
|
|||||||
if (_isConnected && _modbusMaster != null)
|
if (_isConnected && _modbusMaster != null)
|
||||||
{
|
{
|
||||||
_modbusMaster.WriteSingleCoil(0x01, (ushort)PauseControlCoil, false);
|
_modbusMaster.WriteSingleCoil(0x01, (ushort)PauseControlCoil, false);
|
||||||
|
Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -119,23 +119,58 @@ namespace PLCDataMonitor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重建连续时间轴(秒)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 【修复】重建连续时间轴(秒)- 自动扣除暂停时间
|
||||||
List<double> timeAxis = new List<double>();
|
List<double> timeAxis = new List<double>();
|
||||||
DateTime firstValid = filteredData.First().Item1;
|
DateTime effectiveStartTime = filteredData.First().Item1;
|
||||||
double accumulated = 0;
|
DateTime effectiveEndTime = filteredData.Last().Item1;
|
||||||
|
|
||||||
for (int i = 0; i < filteredData.Count; i++)
|
for (int i = 0; i < filteredData.Count; i++)
|
||||||
{
|
{
|
||||||
if (i == 0)
|
DateTime currentTime = filteredData[i].Item1;
|
||||||
timeAxis.Add(0);
|
|
||||||
else
|
// 1. 计算从开始到现在的总物理时间
|
||||||
|
double totalElapsed = (currentTime - effectiveStartTime).TotalSeconds;
|
||||||
|
|
||||||
|
// 2. 计算这段时间内包含的“暂停时长”总和
|
||||||
|
double totalPauseDuration = 0;
|
||||||
|
foreach (var p in pausePeriods)
|
||||||
{
|
{
|
||||||
accumulated += (filteredData[i].Item1 - filteredData[i - 1].Item1).TotalSeconds;
|
// 只计算在当前点之前发生的暂停
|
||||||
timeAxis.Add(accumulated);
|
if (p.end <= effectiveStartTime) continue;
|
||||||
|
if (p.start >= currentTime) break;
|
||||||
|
|
||||||
|
// 计算暂停段与 [开始时间,当前时间] 的重叠部分
|
||||||
|
long overlapStart = p.start > effectiveStartTime ? p.start.Ticks : effectiveStartTime.Ticks;
|
||||||
|
long overlapEnd = p.end < currentTime ? p.end.Ticks : currentTime.Ticks;
|
||||||
|
|
||||||
|
if (overlapEnd > overlapStart)
|
||||||
|
{
|
||||||
|
totalPauseDuration += (overlapEnd - overlapStart) / (double)TimeSpan.TicksPerSecond;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 有效时间 = 物理时间 - 暂停时间
|
||||||
|
double validTime = totalElapsed - totalPauseDuration;
|
||||||
|
timeAxis.Add(Math.Max(0, validTime));
|
||||||
}
|
}
|
||||||
double totalValidSeconds = accumulated; // 有效总时长
|
|
||||||
DateTime effectiveStartTime = firstValid;
|
double totalValidSeconds = timeAxis.Last(); // 更新有效总时长
|
||||||
DateTime effectiveEndTime = filteredData.Last().Item1;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 计算最大摩擦力(基于过滤后的数据)
|
// 计算最大摩擦力(基于过滤后的数据)
|
||||||
double maxFriction1 = filteredData.Max(x => Math.Abs(x.Item2));
|
double maxFriction1 = filteredData.Max(x => Math.Abs(x.Item2));
|
||||||
|
|||||||
@@ -2,30 +2,30 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:PLCDataMonitor">
|
xmlns:local="clr-namespace:PLCDataMonitor">
|
||||||
|
|
||||||
<Style TargetType="{x:Type local:AlarmScrollingText}">
|
<Style TargetType="local:AlarmScrollingText">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="{x:Type local:AlarmScrollingText}">
|
<ControlTemplate TargetType="local:AlarmScrollingText">
|
||||||
<Border Name="PART_ClipBorder"
|
<Border x:Name="PART_ClipBorder"
|
||||||
Background="{TemplateBinding Background}"
|
Background="{TemplateBinding Background}"
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
BorderThickness="{TemplateBinding BorderThickness}"
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
ClipToBounds="True"
|
CornerRadius="4"
|
||||||
Height="40">
|
ClipToBounds="True"
|
||||||
<TextBlock Name="PART_TextBlock"
|
Padding="5,5">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
|
||||||
|
<TextBlock x:Name="PART_TextBlock"
|
||||||
Text="{TemplateBinding Text}"
|
Text="{TemplateBinding Text}"
|
||||||
FontSize="18"
|
FontSize="14"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Foreground="Red"
|
Foreground="Red"
|
||||||
VerticalAlignment="Center"
|
TextWrapping="NoWrap"
|
||||||
RenderTransformOrigin="0,0.5">
|
VerticalAlignment="Center"/>
|
||||||
<TextBlock.RenderTransform>
|
</ScrollViewer>
|
||||||
<TranslateTransform X="0"/>
|
|
||||||
</TextBlock.RenderTransform>
|
|
||||||
</TextBlock>
|
|
||||||
</Border>
|
</Border>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="AlarmScrollingText.cs">
|
<Compile Update="AlarmScrollingText.cs">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user