<Window x:Class="_9696999.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:_9696999"
mc:Ignorable="d"
Title="PLC监控系统" Height="800" Width="1400" Closed="Window_Closed">
<Window.Resources>
<local:StatusColorConverter x:Key="StatusColorConverter"/>
<!-- 展开面板动画 -->
<Storyboard x:Key="ExpandPanel">
<DoubleAnimation Storyboard.TargetProperty="Width"
To="300" Duration="0:0:0.3"/>
</Storyboard>
<Storyboard x:Key="CollapsePanel">
<DoubleAnimation Storyboard.TargetProperty="Width"
To="0" Duration="0:0:0.3"/>
</Storyboard>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- PLC设置面板(可折叠) -->
<Border x:Name="plcPanel" Grid.Column="0" Width="0" Background="#FFE0E0E0" BorderBrush="Gray" BorderThickness="0,0,1,0">
<GroupBox Header="PLC连接设置" Margin="5">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0,5">
<TextBlock Text="站号:" VerticalAlignment="Center" Margin="5" Width="40"/>
<TextBox x:Name="txtStationNumber" Width="50" Margin="5" Text="1"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10">
<Button x:Name="btnConnect" Content="连接PLC" Margin="5" Click="Connect_Click" Width="80"/>
<Button x:Name="btnDisconnect" Content="断开连接" Margin="5" Click="Disconnect_Click" Width="80" IsEnabled="False"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10">
<TextBlock Text="连接状态:" Margin="5" VerticalAlignment="Center" Width="60"/>
<TextBlock x:Name="tbConnectionStatus" Text="未连接" Margin="5" VerticalAlignment="Center" FontWeight="Bold" Foreground="Red"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5">
<TextBlock Text="心跳状态:" Margin="5" VerticalAlignment="Center" Width="60"/>
<TextBlock x:Name="tbHeartbeatStatus" Text="未连接" Margin="5" VerticalAlignment="Center" FontWeight="Bold" Foreground="Gray"/>
</StackPanel>
</StackPanel>
</GroupBox>
</Border>
<!-- 折叠/展开按钮 -->
<Button x:Name="btnTogglePanel" Grid.Column="0" Content="▶" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="5,50,0,0" Width="25" Height="25" FontSize="10" Background="#FFE0E0E0"
Click="TogglePanel_Click" ToolTip="展开PLC设置"/>
<!-- 左侧区域 -->
<Grid Grid.Column="1" Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="220"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 电机监控配置 -->
<GroupBox Grid.Row="0" Header="电机监控点位配置" Margin="0,5,5,3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="点位名称:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtPointName" Width="120" Margin="5"/>
<TextBlock Text="点位地址:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtPointAddress" Width="80" Margin="5"/>
<TextBlock Text="标准值:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtStandardValue" Width="80" Margin="5"/>
<Button x:Name="btnAddPoint" Content="添加点位" Margin="5" Click="AddMonitorPoint_Click" Width="80"/>
<Button x:Name="btnSaveMotorConfig" Content="保存配置" Margin="5" Click="SaveMotorConfig_Click" Width="80"/>
<Button x:Name="btnLoadMotorConfig" Content="加载配置" Margin="5" Click="LoadMotorConfig_Click" Width="80"/>
</StackPanel>
</GroupBox>
<!-- 电机监控数据表格 -->
<GroupBox Grid.Row="1" Header="电机点位实时监控" Margin="0,3,5,3">
<DataGrid ItemsSource="{Binding MotorDataList}" AutoGenerateColumns="False" Margin="5">
<DataGrid.Columns>
<DataGridTextColumn Header="点位名称" Binding="{Binding PointName}" Width="120"/>
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="80"/>
<DataGridTextColumn Header="标准值" Binding="{Binding StandardValue}" Width="80"/>
<DataGridTextColumn Header="实际值" Binding="{Binding ActualValue}" Width="80"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status}" Width="100">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground"
Value="{Binding Status, Converter={StaticResource StatusColorConverter}}"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- 日志区域 -->
<GroupBox Grid.Row="2" Header="通讯日志" Margin="0,3,5,5">
<TextBox x:Name="txtCommunicationLog"
IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap"
FontFamily="Consolas"
Height="177"/>
</GroupBox>
<!--设备节拍时间/设备伺服电机负载率历史数据查看-->
<GroupBox Grid.Row="3" Header="节拍时间和负载率历史数据查看" Margin="0,3,5,5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="数据类型:" VerticalAlignment="Center" Margin="5"/>
<ComboBox x:Name="cmbDataType" Width="100" Margin="5">
<ComboBoxItem Content="节拍时间"/>
<ComboBoxItem Content="负载率"/>
</ComboBox>
<TextBlock Text="选择日期:" VerticalAlignment="Center" Margin="5"/>
<DatePicker x:Name="dpHistoryDate" Width="120" Margin="5"/>
<Button x:Name="btnLoadHistory" Content="加载历史数据"
Margin="5" Click="LoadHistoryData_Click" Width="100"/>
<Button x:Name="btnClearHistory" Content="清除图表"
Margin="5" Click="ClearHistoryChart_Click" Width="80"/>
</StackPanel>
<Canvas x:Name="canvasHistoryChart" Grid.Row="1" Background="White" Margin="5"/>
</Grid>
</GroupBox>
</Grid>
<!-- 右侧区域 -->
<Grid Grid.Column="2" Margin="5,0,5,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 节拍监控配置 -->
<GroupBox Grid.Row="0" Header="节拍时间监控配置" Margin="5,5,0,3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="部位名称:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtSectionName" Width="120" Margin="5"/>
<TextBlock Text="监控地址:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtSectionAddress" Width="80" Margin="5"/>
<Button x:Name="btnAddSection" Content="添加部位" Margin="5" Click="AddSection_Click" Width="80"/>
<Button x:Name="btnSaveRhythmConfig" Content="保存配置" Margin="5" Click="SaveRhythmConfig_Click" Width="80"/>
<Button x:Name="btnLoadRhythmConfig" Content="加载配置" Margin="5" Click="LoadRhythmConfig_Click" Width="80"/>
</StackPanel>
</GroupBox>
<!-- 节拍时间图表 -->
<GroupBox Grid.Row="1" Header="设备节拍时间监控" Margin="5,3,0,3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Canvas x:Name="canvasRhythmChart" Background="White"/>
<StackPanel Grid.Column="1" Orientation="Vertical" VerticalAlignment="Center" Margin="20,0,10,0">
<Rectangle Width="20" Height="10" Fill="Green" Margin="0,5"/>
<TextBlock Text="< 10s" Margin="0,5"/>
<Rectangle Width="20" Height="10" Fill="YellowGreen" Margin="0,5"/>
<TextBlock Text="10-20s" Margin="0,5"/>
<Rectangle Width="20" Height="10" Fill="Orange" Margin="0,5"/>
<TextBlock Text="20-30s" Margin="0,5"/>
<Rectangle Width="20" Height="10" Fill="Red" Margin="0,5"/>
<TextBlock Text="> 30s" Margin="0,5"/>
</StackPanel>
</Grid>
</GroupBox>
<!-- 负载监控配置 -->
<GroupBox Grid.Row="2" Header="伺服电机负载率监控配置" Margin="5,3,0,3">
<StackPanel Orientation="Horizontal">
<TextBlock Text="电机名称:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtMotorName" Width="120" Margin="5"/>
<TextBlock Text="监控地址:" VerticalAlignment="Center" Margin="5"/>
<TextBox x:Name="txtMotorAddress" Width="80" Margin="5"/>
<Button x:Name="btnAddMotor" Content="添加电机" Margin="5" Click="AddMotor_Click" Width="80"/>
<Button x:Name="btnSaveLoadConfig" Content="保存配置" Margin="5" Click="SaveLoadConfig_Click" Width="80"/>
<Button x:Name="btnLoadLoadConfig" Content="加载配置" Margin="5" Click="LoadLoadConfig_Click" Width="80"/>
</StackPanel>
</GroupBox>
<!-- 负载率图表 -->
<GroupBox Grid.Row="3" Header="伺服电机负载率监控" Margin="5,3,0,5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Canvas x:Name="canvasLoadChart" Background="White"/>
<StackPanel Grid.Column="1" Orientation="Vertical" VerticalAlignment="Center" Margin="20,0,10,0">
<Rectangle Width="20" Height="10" Fill="Green" Margin="0,5"/>
<TextBlock Text="< 80%" Margin="0,5"/>
<Rectangle Width="20" Height="10" Fill="Red" Margin="0,5"/>
<TextBlock Text="≥ 80%" Margin="0,5"/>
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using ActUtlType64Lib;
using Microsoft.Win32;
using System.Text.Json.Serialization;
using System.Windows.Media.Animation;
using System.Text;
using Path = System.IO.Path;
using System.Globalization;
namespace _9696999
{
public partial class MainWindow : Window
{
//设备节拍时间负载率保存
private string _saveDirectory = @"D:\PLC_Monitor_Data";
private StreamWriter? _rhythmDataWriter;
private StreamWriter? _loadDataWriter;
private DateTime _lastFileCreationDate;
private PlcMonitorViewModel _viewModel;
private ActUtlType64 _plc;
private DispatcherTimer _readTimer;
private DispatcherTimer _logClearTimer;
private DispatcherTimer _heartbeatTimer; // 心跳定时器
private bool _isConnected = false;
private int _stationNumber = 1; // 保存站号用于重连
private int _connectionRetryCount = 0; // 重试次数计数
private const int MAX_RETRY_COUNT = 5; // 最大重试次数
// 存储上一次的值用于比较变化
private ObservableCollection<float> _lastMotorValues = new ObservableCollection<float>();
private ObservableCollection<float> _lastRhythmValues = new ObservableCollection<float>();
private ObservableCollection<float> _lastLoadValues = new ObservableCollection<float>();
public MainWindow()
{
InitializeComponent();
_viewModel = new PlcMonitorViewModel();
DataContext = _viewModel;
_plc = new ActUtlType64();
_readTimer = new DispatcherTimer();
_logClearTimer = new DispatcherTimer();
_heartbeatTimer = new DispatcherTimer(); // 初始化心跳定时器
InitializePlc();
InitializeLogClearTimer();
InitializeHeartbeatTimer(); // 初始化心跳定时器
InitializeDataSaving(); //数据保存
}
private void InitializeDataSaving()
{
// 创建保存目录
if (!Directory.Exists(_saveDirectory))
{
Directory.CreateDirectory(_saveDirectory);
}
// 初始化文件写入器
CreateDataFiles();
AddLog($"数据将实时保存到: {_saveDirectory}");
}
private void CreateDataFiles()
{
try
{
// 关闭现有文件流
_rhythmDataWriter?.Close();
_loadDataWriter?.Close();
string dateStr = DateTime.Today.ToString("yyyyMMdd");
string rhythmFilePath = $"{_saveDirectory}\\rhythm_{dateStr}.csv";
string loadFilePath = $"{_saveDirectory}\\load_{dateStr}.csv";
// 确保目录存在
if (!Directory.Exists(_saveDirectory))
{
Directory.CreateDirectory(_saveDirectory);
}
// 使用FileMode.OpenOrCreate和FileShare.ReadWrite模式
if (!File.Exists(rhythmFilePath))
{
using (var fs = new FileStream(rhythmFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
using (var writer = new StreamWriter(fs, Encoding.UTF8))
{
writer.WriteLine("时间,名称,地址,数值");
}
}
}
if (!File.Exists(loadFilePath))
{
using (var fs = new FileStream(loadFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
using (var writer = new StreamWriter(fs, Encoding.UTF8))
{
writer.WriteLine("时间,名称,地址,数值");
}
}
}
// 以共享模式打开写入器(允许其他进程读取)
var rhythmFs = new FileStream(rhythmFilePath,
FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
_rhythmDataWriter = new StreamWriter(rhythmFs, Encoding.UTF8);
_rhythmDataWriter.AutoFlush = true;
var loadFs = new FileStream(loadFilePath,
FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
_loadDataWriter = new StreamWriter(loadFs, Encoding.UTF8);
_loadDataWriter.AutoFlush = true;
_lastFileCreationDate = DateTime.Today;
}
catch (Exception ex)
{
AddLog($"创建数据文件失败: {ex.Message}");
_rhythmDataWriter = null;
_loadDataWriter = null;
}
}
private void SaveRhythmData(ChartData data, float value)
{
try
{
// 每天创建新文件
if (DateTime.Today != _lastFileCreationDate)
{
CreateDataFiles();
}
// 检查写入器是否已初始化
if (_rhythmDataWriter == null)
{
AddLog("节拍数据写入器未初始化,跳过保存");
return;
}
// 写入CSV数据
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
string csvLine = $"{timestamp},{data.Name},{data.Address},{value}";
_rhythmDataWriter.WriteLine(csvLine);
}
catch (Exception ex)
{
AddLog($"保存节拍数据失败: {ex.Message}");
}
}
private void SaveLoadData(ChartData data, float value)
{
try
{
// 每天创建新文件
if (DateTime.Today != _lastFileCreationDate)
{
CreateDataFiles();
}
// 检查写入器是否已初始化
if (_loadDataWriter == null)
{
AddLog("负载数据写入器未初始化,跳过保存");
return;
}
// 写入CSV数据
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
string csvLine = $"{timestamp},{data.Name},{data.Address},{value}";
_loadDataWriter.WriteLine(csvLine);
}
catch (Exception ex)
{
AddLog($"保存负载数据失败: {ex.Message}");
}
}
private void InitializeHeartbeatTimer()
{
_heartbeatTimer.Interval = TimeSpan.FromSeconds(5); // 5秒检测一次
_heartbeatTimer.Tick += HeartbeatTimer_Tick;
_heartbeatTimer.Start();
AddLog("心跳检测功能已启动");
}
private void HeartbeatTimer_Tick(object? sender, EventArgs e)
{
// 只有在已连接状态下才检测心跳
if (!_isConnected) return;
try
{
// 尝试读取一个测试点来检测连接状态
short testValue;
int result = _plc.ReadDeviceBlock2("D0", 1, out testValue);
if (result != 0)
{
// 读取失败,连接可能已断开
_isConnected = false;
_readTimer.Stop();
Application.Current.Dispatcher.Invoke(() =>
{
tbConnectionStatus.Text = "连接断开 (尝试重连中...)";
tbHeartbeatStatus.Text = "连接异常";
tbHeartbeatStatus.Foreground = Brushes.Red;
});
AddLog($"心跳检测失败,错误代码: {result}");
TryReconnect();
}
else
{
// 连接正常
Application.Current.Dispatcher.Invoke(() =>
{
tbHeartbeatStatus.Text = "连接正常";
tbHeartbeatStatus.Foreground = Brushes.Green;
});
}
}
catch (Exception ex)
{
AddLog($"心跳检测异常: {ex.Message}");
_isConnected = false;
_readTimer.Stop();
TryReconnect();
}
}
private void TryReconnect()
{
if (_connectionRetryCount >= MAX_RETRY_COUNT)
{
AddLog($"已达到最大重试次数({MAX_RETRY_COUNT}),停止自动重连");
_heartbeatTimer.Stop();
return;
}
_connectionRetryCount++;
AddLog($"尝试第 {_connectionRetryCount} 次重新连接...");
try
{
_plc.Close(); // 确保先关闭连接
int result = _plc.Open();
if (result == 0)
{
_isConnected = true;
_connectionRetryCount = 0; // 重置重试计数
Application.Current.Dispatcher.Invoke(() =>
{
tbConnectionStatus.Text = $"已重新连接 (站号: {_stationNumber})";
tbHeartbeatStatus.Text = "连接正常";
tbHeartbeatStatus.Foreground = Brushes.Green;
btnConnect.IsEnabled = false;
btnDisconnect.IsEnabled = true;
});
_readTimer.Start();
AddLog($"PLC重新连接成功 (站号: {_stationNumber})");
}
else
{
AddLog($"重新连接失败,错误代码: {result}");
}
}
catch (Exception ex)
{
AddLog($"重新连接异常: {ex.Message}");
}
}
private void InitializeLogClearTimer()
{
_logClearTimer = new DispatcherTimer();
// 设置间隔为10分钟(600,000毫秒)
_logClearTimer.Interval = TimeSpan.FromMinutes(1);
_logClearTimer.Tick += LogClearTimer_Tick;
_logClearTimer.Start();
AddLog("日志自动清除功能已启动(每隔10分钟清除一次)");
}
private void LogClearTimer_Tick(object? sender, EventArgs e)
{
ClearCommunicationLog();
}
private void ClearCommunicationLog()
{
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(ClearCommunicationLog);
return;
}
// 记录清除操作
string clearMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} 已自动清除通讯日志";
// 清除日志
txtCommunicationLog.Text = clearMessage + Environment.NewLine;
// 可选:添加日志记录表明已执行清除操作
AddLog("通讯日志已自动清除");
}
private void InitializePlc()
{
try
{
_plc = new ActUtlType64();
AddLog("PLC通信库初始化成功");
}
catch (Exception ex)
{
MessageBox.Show($"初始化PLC通信失败: {ex.Message}\n请确保已安装MX Component运行时库。");
AddLog($"PLC通信库初始化失败: {ex.Message}");
}
}
//UI界面隐藏触发Click
private bool _isPanelExpanded = false;
private void TogglePanel_Click(object sender, RoutedEventArgs e)
{
if (_isPanelExpanded)
{
// 收起面板
Storyboard collapseAnimation = (Storyboard)FindResource("CollapsePanel");
collapseAnimation.Begin(plcPanel);
btnTogglePanel.Content = "▶";
btnTogglePanel.ToolTip = "展开PLC设置";
}
else
{
// 展开面板
Storyboard expandAnimation = (Storyboard)FindResource("ExpandPanel");
expandAnimation.Begin(plcPanel);
btnTogglePanel.Content = "◀";
btnTogglePanel.ToolTip = "收起PLC设置";
}
_isPanelExpanded = !_isPanelExpanded;
}
// 连接PLC
private void Connect_Click(object sender, RoutedEventArgs e)
{
// 验证站号输入
if (!int.TryParse(txtStationNumber.Text, out int stationNumber) || stationNumber < 1 || stationNumber > 255)
{
MessageBox.Show("请输入有效的站号 (1-255)");
return;
}
_stationNumber = stationNumber; // 保存站号用于重连
_connectionRetryCount = 0; // 重置重试计数
try
{
// 设置站号
_plc.ActLogicalStationNumber = stationNumber;
int result = _plc.Open();
if (result == 0) // 返回0表示成功
{
_isConnected = true;
tbConnectionStatus.Text = $"已连接 (站号: {stationNumber})";
tbHeartbeatStatus.Text = "连接正常";
tbHeartbeatStatus.Foreground = Brushes.Green;
btnConnect.IsEnabled = false;
btnDisconnect.IsEnabled = true;
txtStationNumber.IsEnabled = false;
StartMonitoring();
AddLog($"PLC连接成功 (站号: {stationNumber})");
}
else
{
MessageBox.Show($"连接失败,错误代码: {result}");
AddLog($"PLC连接失败,错误代码: {result}");
}
}
catch (Exception ex)
{
MessageBox.Show($"连接失败: {ex.Message}");
AddLog($"PLC连接异常: {ex.Message}");
}
}
// 启动监控定时器
private void StartMonitoring()
{
_readTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(200) // 200ms采集周期
};
_readTimer.Tick += async (s, e) => await ReadPlcDataAsync();
_readTimer.Start();
AddLog("开始监控PLC数据");
}
// 异步读取PLC数据
private async System.Threading.Tasks.Task ReadPlcDataAsync()
{
if (!_isConnected) return;
try
{
await System.Threading.Tasks.Task.Run(() =>
{
// 读取电机点位数据
ReadMotorData();
// 读取节拍时间数据
ReadRhythmData();
// 读取负载率数据
ReadLoadData();
});
}
catch (Exception ex)
{
AddLog($"读取数据异常: {ex.Message}");
}
}
// 读取电机点位数据
private void ReadMotorData()
{
for (int i = 0; i < _viewModel.MotorDataList.Count; i++)
{
short value = 0;
int result = _plc.ReadDeviceBlock2(_viewModel.MotorDataList[i].Address, 1, out value);
if (result == 0)
{
float actualValue = value;
Application.Current.Dispatcher.Invoke(() =>
{
var data = _viewModel.MotorDataList[i];
data.ActualValue = actualValue;
// 检查值是否发生变化
if (_lastMotorValues.Count <= i)
{
_lastMotorValues.Add(actualValue);
data.Status = (actualValue == data.StandardValue) ? "正常" : "异常";
if (data.Status == "异常")
{
AddLog($"点位 {data.PointName} 异常: 实际值 {actualValue} ≠ 标准值 {data.StandardValue}");
}
else
{
AddLog("点位数据已恢复正常");
}
}
else if (_lastMotorValues[i] != actualValue)
{
data.Status = (actualValue == data.StandardValue) ? "正常" : "异常";
if (data.Status == "异常")
{
AddLog($"点位 {data.PointName} 异常: 实际值 {actualValue} ≠ 标准值 {data.StandardValue}");
}
else
{
AddLog("点位数据已恢复正常");
}
_lastMotorValues[i] = actualValue;
}
});
}
else
{
AddLog($"读取点位 {_viewModel.MotorDataList[i].PointName} 失败,错误代码: {result}");
}
}
}
// 读取节拍时间数据并绘制柱状图
// 在读取节拍数据后立即保存
private void ReadRhythmData()
{
for (int i = 0; i < _viewModel.RhythmDataList.Count; i++)
{
short value = 0;
int result = _plc.ReadDeviceBlock2(_viewModel.RhythmDataList[i].Address, 1, out value);
if (result == 0)
{
float rhythmValue = value / 10.0f;
Application.Current.Dispatcher.Invoke(() =>
{
_viewModel.RhythmDataList[i].Value = rhythmValue;
DrawBarChart(canvasRhythmChart, _viewModel.RhythmDataList, true, "rhythm");
});
// 立即保存节拍数据
SaveRhythmData(_viewModel.RhythmDataList[i], rhythmValue);
}
else
{
AddLog($"读取节拍数据 {_viewModel.RhythmDataList[i].Name} 失败,错误代码: {result}");
}
}
}
// 读取负载率数据并绘制柱状图
// 在读取负载数据后立即保存
private void ReadLoadData()
{
for (int i = 0; i < _viewModel.LoadDataList.Count; i++)
{
short value = 0;
int result = _plc.ReadDeviceBlock2(_viewModel.LoadDataList[i].Address, 1, out value);
if (result == 0)
{
float loadValue = value;
Application.Current.Dispatcher.Invoke(() =>
{
_viewModel.LoadDataList[i].Value = value;
DrawBarChart(canvasLoadChart, _viewModel.LoadDataList, true, "load");
});
// 立即保存负载数据
SaveLoadData(_viewModel.LoadDataList[i], loadValue);
}
else
{
AddLog($"读取负载数据 {_viewModel.LoadDataList[i].Name} 失败,错误代码: {result}");
}
}
}
// 绘制柱状图
private void DrawBarChart(Canvas canvas, ObservableCollection<ChartData> dataList, bool showValue, string chartType)
{
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(() => DrawBarChart(canvas, dataList, showValue, chartType));
return;
}
canvas.Children.Clear();
if (dataList.Count == 0) return;
double canvasWidth = canvas.ActualWidth;
double canvasHeight = canvas.ActualHeight;
double barWidth = (canvasWidth - 40) / dataList.Count - 10;
double maxValue = GetMaxValue(dataList);
// 绘制Y轴
Line yAxis = new Line
{
X1 = 30,
Y1 = 20,
X2 = 30,
Y2 = canvasHeight - 30,
Stroke = Brushes.Black,
StrokeThickness = 2
};
canvas.Children.Add(yAxis);
// 绘制X轴
Line xAxis = new Line
{
X1 = 30,
Y1 = canvasHeight - 30,
X2 = canvasWidth - 10,
Y2 = canvasHeight - 30,
Stroke = Brushes.Black,
StrokeThickness = 2
};
canvas.Children.Add(xAxis);
// 绘制Y轴刻度
for (int i = 0; i <= 5; i++)
{
double yPos = canvasHeight - 30 - (i * (canvasHeight - 50) / 5);
double value = i * maxValue / 5;
Line tick = new Line
{
X1 = 25,
Y1 = yPos,
X2 = 30,
Y2 = yPos,
Stroke = Brushes.Black,
StrokeThickness = 1
};
canvas.Children.Add(tick);
TextBlock tickValue = new TextBlock
{
Text = value.ToString("F1"),
FontSize = 10,
Foreground = Brushes.Black
};
Canvas.SetLeft(tickValue, 5);
Canvas.SetTop(tickValue, yPos - 8);
canvas.Children.Add(tickValue);
}
for (int i = 0; i < dataList.Count; i++)
{
double barHeight = (dataList[i].Value / maxValue) * (canvasHeight - 60);
double x = 40 + i * (barWidth + 10);
double y = canvasHeight - 30 - barHeight;
// 根据图表类型和数值确定颜色
Brush barColor = GetBarColor(chartType, dataList[i].Value);
// 绘制柱形
System.Windows.Shapes.Rectangle rect = new System.Windows.Shapes.Rectangle
{
Width = barWidth,
Height = barHeight,
Fill = barColor,
Stroke = Brushes.Black,
StrokeThickness = 1
};
Canvas.SetLeft(rect, x);
Canvas.SetTop(rect, y);
canvas.Children.Add(rect);
// 绘制数值
if (showValue)
{
TextBlock text = new TextBlock
{
Text = dataList[i].Value.ToString("F1"),
FontSize = 10,
Foreground = Brushes.Black
};
Canvas.SetLeft(text, x + barWidth / 2 - 10);
Canvas.SetTop(text, y - 20);
canvas.Children.Add(text);
}
// 绘制名称
TextBlock nameText = new TextBlock
{
Text = $"{dataList[i].Name}\n{dataList[i].Address}",
FontSize = 10,
Foreground = Brushes.Black,
TextAlignment = TextAlignment.Center,
Width = barWidth + 10
};
Canvas.SetLeft(nameText, x - 5);
Canvas.SetTop(nameText, canvasHeight - 25);
canvas.Children.Add(nameText);
}
}
//历史数据
private void LoadHistoryData_Click(object sender, RoutedEventArgs e)
{
// 安全获取 ComboBox 的选中项
if (cmbDataType.SelectedItem is not ComboBoxItem selectedItem ||
selectedItem.Content == null)
{
MessageBox.Show("请选择有效的项目");
return;
}
string? dataType = selectedItem.Content.ToString();
// 检查是否为空
if (string.IsNullOrWhiteSpace(dataType))
{
MessageBox.Show("所选数据类型无效");
return;
}
if (dpHistoryDate.SelectedDate == null)
{
MessageBox.Show("请选择日期");
return;
}
DateTime selectedDate = dpHistoryDate.SelectedDate.Value;
LoadAndDrawHistoryData(dataType!, selectedDate); // 使用 ! 表示我们知道它不是 null
}
private void LoadAndDrawHistoryData(string dataType, DateTime date)
{
try
{
// 添加空值检查
if (string.IsNullOrEmpty(dataType))
{
MessageBox.Show("数据类型无效");
return;
}
canvasHistoryChart.Children.Clear();
AddLog($"开始加载{dataType}历史数据 ({date:yyyy-MM-dd})");
// 根据数据类型构建文件路径
string fileName = dataType == "节拍时间" ? $"rhythm_{date:yyyyMMdd}.csv" : $"load_{date:yyyyMMdd}.csv";
string filePath = Path.Combine(_saveDirectory, fileName);
// 文件不存在处理
if (!File.Exists(filePath))
{
MessageBox.Show($"找不到{dataType}的历史数据文件");
AddLog($"未找到历史数据文件: {filePath}");
return;
}
// 读取数据
var lines = File.ReadAllLines(filePath).Skip(1); // 跳过标题行
var dataPoints = new List<Tuple<DateTime, float>>();
// 修改时间解析部分
foreach (var line in lines)
{
var parts = line.Split(',');
if (parts.Length < 4) continue;
// 使用确切格式解析时间戳
if (DateTime.TryParseExact(parts[0], "yyyy-MM-dd HH:mm:ss.fff",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime timestamp) &&
float.TryParse(parts[3], out float value))
{
dataPoints.Add(new Tuple<DateTime, float>(timestamp, value));
}
}
if (dataPoints.Count == 0)
{
MessageBox.Show("没有找到有效数据");
return;
}
// 绘制图表
DrawHistoryChart(dataPoints, dataType);
AddLog($"已加载{dataType}历史数据: {dataPoints.Count}条记录");
}
catch (Exception ex)
{
MessageBox.Show($"加载历史数据失败: {ex.Message}");
AddLog($"加载历史数据出错: {ex.Message}");
}
}
private void DrawHistoryChart(List<Tuple<DateTime, float>> dataPoints, string dataType)
{
double canvasWidth = canvasHistoryChart.ActualWidth;
double canvasHeight = canvasHistoryChart.ActualHeight;
const double margin = 30;
// 清除现有内容
canvasHistoryChart.Children.Clear();
if (canvasWidth <= 0 || canvasHeight <= 0) return;
// 计算最小/最大时间和值
var minTime = dataPoints.Min(p => p.Item1);
var maxTime = dataPoints.Max(p => p.Item1);
var minValue = dataPoints.Min(p => p.Item2);
var maxValue = dataPoints.Max(p => p.Item2);
double timeRange = (maxTime - minTime).TotalSeconds;
double valueRange = maxValue - minValue;
if (valueRange == 0) valueRange = 1; // 防止除零
// 绘制坐标轴
DrawHistoryAxis(minTime, maxTime, minValue, maxValue, canvasWidth, canvasHeight, margin);
// 绘制数据点
for (int i = 0; i < dataPoints.Count - 1; i++)
{
var p1 = dataPoints[i];
var p2 = dataPoints[i + 1];
double x1 = margin + ((p1.Item1 - minTime).TotalSeconds / timeRange) * (canvasWidth - 2 * margin);
double y1 = canvasHeight - margin - ((p1.Item2 - minValue) / valueRange) * (canvasHeight - 2 * margin);
double x2 = margin + ((p2.Item1 - minTime).TotalSeconds / timeRange) * (canvasWidth - 2 * margin);
double y2 = canvasHeight - margin - ((p2.Item2 - minValue) / valueRange) * (canvasHeight - 2 * margin);
// 绘制连线
var line = new Line
{
X1 = x1,
Y1 = y1,
X2 = x2,
Y2 = y2,
Stroke = dataType == "节拍时间" ? Brushes.Blue : Brushes.Red,
StrokeThickness = 2
};
canvasHistoryChart.Children.Add(line);
// 绘制数据点
var ellipse = new Ellipse
{
Width = 6,
Height = 6,
Fill = Brushes.Black
};
Canvas.SetLeft(ellipse, x1 - 3);
Canvas.SetTop(ellipse, y1 - 3);
canvasHistoryChart.Children.Add(ellipse);
}
// 添加标题
var title = new TextBlock
{
Text = $"{dataType}历史数据 ({dataPoints.Count}条记录)",
FontWeight = FontWeights.Bold,
FontSize = 14,
Foreground = Brushes.Black
};
Canvas.SetLeft(title, canvasWidth / 2 - 50);
Canvas.SetTop(title, 5);
canvasHistoryChart.Children.Add(title);
}
private void DrawHistoryAxis(DateTime minTime, DateTime maxTime, double minValue, double maxValue,
double canvasWidth, double canvasHeight, double margin)
{
// 绘制X轴(时间轴)
var xAxis = new Line
{
X1 = margin,
Y1 = canvasHeight - margin,
X2 = canvasWidth - margin,
Y2 = canvasHeight - margin,
Stroke = Brushes.Black,
StrokeThickness = 2
};
canvasHistoryChart.Children.Add(xAxis);
// 绘制Y轴(数值轴)
var yAxis = new Line
{
X1 = margin,
Y1 = margin,
X2 = margin,
Y2 = canvasHeight - margin,
Stroke = Brushes.Black,
StrokeThickness = 2
};
canvasHistoryChart.Children.Add(yAxis);
// 添加时间刻度
TimeSpan timeInterval = TimeSpan.FromMinutes(30); // 每30分钟一个刻度
DateTime current = minTime;
while (current <= maxTime)
{
double ratio = (current - minTime).TotalSeconds / (maxTime - minTime).TotalSeconds;
double x = margin + ratio * (canvasWidth - 2 * margin);
// 刻度线
var tick = new Line
{
X1 = x,
Y1 = canvasHeight - margin,
X2 = x,
Y2 = canvasHeight - margin + 5,
Stroke = Brushes.Black
};
canvasHistoryChart.Children.Add(tick);
// 时间标签
var label = new TextBlock
{
Text = current.ToString("HH:mm"),
FontSize = 10,
Foreground = Brushes.Black
};
Canvas.SetLeft(label, x - 15);
Canvas.SetTop(label, canvasHeight - margin + 10);
canvasHistoryChart.Children.Add(label);
current = current.Add(timeInterval);
}
// 添加数值刻度
double valueInterval = (maxValue - minValue) / 5;
for (int i = 0; i <= 5; i++)
{
double value = minValue + i * valueInterval;
double y = canvasHeight - margin - (i / 5.0) * (canvasHeight - 2 * margin);
// 刻度线
var tick = new Line
{
X1 = margin,
Y1 = y,
X2 = margin - 5,
Y2 = y,
Stroke = Brushes.Black
};
canvasHistoryChart.Children.Add(tick);
// 数值标签
var label = new TextBlock
{
Text = value.ToString("F1"),
FontSize = 10,
Foreground = Brushes.Black
};
Canvas.SetRight(label, canvasWidth - margin + 5);
Canvas.SetTop(label, y - 8);
canvasHistoryChart.Children.Add(label);
}
}
private void ClearHistoryChart_Click(object sender, RoutedEventArgs e)
{
// 清除历史图表区域
canvasHistoryChart.Children.Clear();
// 添加日志记录清理操作
AddLog("已清除历史数据图表");
}
// 根据图表类型和数值获取对应的颜色
private Brush GetBarColor(string chartType, double value)
{
switch (chartType)
{
case "rhythm": // 设备节拍时间
if (value < 10) return Brushes.Green;
else if (value >= 10 && value < 20) return Brushes.YellowGreen;
else if (value >= 20 && value < 30) return Brushes.Orange;
else return Brushes.Red;
case "load": // 伺服电机负载率
return value < 80 ? Brushes.Green : Brushes.Red;
default: // 默认颜色
return Brushes.SteelBlue;
}
}
// 获取最大值用于比例计算
private float GetMaxValue(ObservableCollection<ChartData> dataList)
{
float max = 0;
foreach (var item in dataList)
{
if (item.Value > max) max = item.Value;
}
return max > 0 ? max : 1;
}
// 添加日志
private void AddLog(string message)
{
Application.Current.Dispatcher.Invoke(() =>
{
txtCommunicationLog.AppendText($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} {message}\n");
txtCommunicationLog.ScrollToEnd();
});
}
// 电机点位配置保存
private void SaveMotorConfig_Click(object sender, RoutedEventArgs e)
{
try
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
saveFileDialog.DefaultExt = "json";
saveFileDialog.InitialDirectory = @"D:\"; // 默认指向D盘[1](@ref)
saveFileDialog.FileName = "motor_config.json";
if (saveFileDialog.ShowDialog() == true)
{
string jsonString = JsonSerializer.Serialize(_viewModel.MotorDataList);
File.WriteAllText(saveFileDialog.FileName, jsonString);
AddLog($"电机配置已保存到: {saveFileDialog.FileName}");
}
}
catch (Exception ex)
{
MessageBox.Show($"保存配置失败: {ex.Message}");
AddLog($"保存电机配置异常: {ex.Message}");
}
}
// 电机点位配置读取
private void LoadMotorConfig_Click(object sender, RoutedEventArgs e)
{
try
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog.DefaultExt = "json";
openFileDialog.InitialDirectory = @"D:\"; // 默认指向D盘
if (openFileDialog.ShowDialog() == true)
{
string jsonString = File.ReadAllText(openFileDialog.FileName);
var data = JsonSerializer.Deserialize<ObservableCollection<MotorPointData>>(jsonString);
if (data != null)
{
_viewModel.MotorDataList.Clear();
foreach (var item in data)
{
_viewModel.MotorDataList.Add(item);
}
AddLog($"电机配置已从 {openFileDialog.FileName} 读取");
}
}
}
catch (Exception ex)
{
MessageBox.Show($"读取配置失败: {ex.Message}");
AddLog($"读取电机配置异常: {ex.Message}");
}
}
// 节拍配置保存
private void SaveRhythmConfig_Click(object sender, RoutedEventArgs e)
{
try
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
saveFileDialog.DefaultExt = "json";
saveFileDialog.InitialDirectory = @"D:\";
saveFileDialog.FileName = "rhythm_config.json";
if (saveFileDialog.ShowDialog() == true)
{
string jsonString = JsonSerializer.Serialize(_viewModel.RhythmDataList);
File.WriteAllText(saveFileDialog.FileName, jsonString);
AddLog($"节拍配置已保存到: {saveFileDialog.FileName}");
}
}
catch (Exception ex)
{
MessageBox.Show($"保存配置失败: {ex.Message}");
AddLog($"保存节拍配置异常: {ex.Message}");
}
}
// 节拍配置读取
private void LoadRhythmConfig_Click(object sender, RoutedEventArgs e)
{
try
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog.DefaultExt = "json";
openFileDialog.InitialDirectory = @"D:\";
if (openFileDialog.ShowDialog() == true)
{
string jsonString = File.ReadAllText(openFileDialog.FileName);
var data = JsonSerializer.Deserialize<ObservableCollection<ChartData>>(jsonString);
if (data != null)
{
_viewModel.RhythmDataList.Clear();
foreach (var item in data)
{
_viewModel.RhythmDataList.Add(item);
}
AddLog($"节拍配置已从 {openFileDialog.FileName} 读取");
}
}
}
catch (Exception ex)
{
MessageBox.Show($"读取配置失败: {ex.Message}");
AddLog($"读取节拍配置异常: {ex.Message}");
}
}
// 负载配置保存
private void SaveLoadConfig_Click(object sender, RoutedEventArgs e)
{
try
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
saveFileDialog.DefaultExt = "json";
saveFileDialog.InitialDirectory = @"D:\";
saveFileDialog.FileName = "load_config.json";
if (saveFileDialog.ShowDialog() == true)
{
string jsonString = JsonSerializer.Serialize(_viewModel.LoadDataList);
File.WriteAllText(saveFileDialog.FileName, jsonString);
AddLog($"负载配置已保存到: {saveFileDialog.FileName}");
}
}
catch (Exception ex)
{
MessageBox.Show($"保存配置失败: {ex.Message}");
AddLog($"保存负载配置异常: {ex.Message}");
}
}
// 负载配置读取
private void LoadLoadConfig_Click(object sender, RoutedEventArgs e)
{
try
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog.DefaultExt = "json";
openFileDialog.InitialDirectory = @"D:\";
if (openFileDialog.ShowDialog() == true)
{
string jsonString = File.ReadAllText(openFileDialog.FileName);
var data = JsonSerializer.Deserialize<ObservableCollection<ChartData>>(jsonString);
if (data != null)
{
_viewModel.LoadDataList.Clear();
foreach (var item in data)
{
_viewModel.LoadDataList.Add(item);
}
AddLog($"负载配置已从 {openFileDialog.FileName} 读取");
}
}
}
catch (Exception ex)
{
MessageBox.Show($"读取配置失败: {ex.Message}");
AddLog($"读取负载配置异常: {ex.Message}");
}
}
// 断开连接
private void Disconnect_Click(object sender, RoutedEventArgs e)
{
try
{
_plc.Close();
_isConnected = false;
_readTimer.Stop();
tbConnectionStatus.Text = "未连接";
tbHeartbeatStatus.Text = "未连接";
tbHeartbeatStatus.Foreground = Brushes.Gray;
btnConnect.IsEnabled = true;
btnDisconnect.IsEnabled = false;
txtStationNumber.IsEnabled = true;
_connectionRetryCount = 0; // 重置重试计数
AddLog("PLC连接已断开");
}
catch (Exception ex)
{
MessageBox.Show($"断开连接失败: {ex.Message}");
AddLog($"断开连接异常: {ex.Message}");
}
}
// 添加监控点
private void AddMonitorPoint_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(txtPointName.Text) ||
string.IsNullOrWhiteSpace(txtPointAddress.Text) ||
string.IsNullOrWhiteSpace(txtStandardValue.Text))
{
MessageBox.Show("请填写完整的监控点信息");
return;
}
if (!float.TryParse(txtStandardValue.Text, out float standardValue))
{
MessageBox.Show("标准值必须是有效的数字");
return;
}
_viewModel.MotorDataList.Add(new MotorPointData
{
PointName = txtPointName.Text,
Address = txtPointAddress.Text,
StandardValue = standardValue,
ActualValue = 0,
Status = "未知"
});
txtPointName.Text = "";
txtPointAddress.Text = "";
txtStandardValue.Text = "";
}
// 添加节拍监控部位
private void AddSection_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(txtSectionName.Text) ||
string.IsNullOrWhiteSpace(txtSectionAddress.Text))
{
MessageBox.Show("请填写完整的部位信息");
return;
}
_viewModel.RhythmDataList.Add(new ChartData
{
Name = txtSectionName.Text,
Address = txtSectionAddress.Text,
Value = 0
});
txtSectionName.Text = "";
txtSectionAddress.Text = "";
}
// 添加负载监控电机
private void AddMotor_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(txtMotorName.Text) ||
string.IsNullOrWhiteSpace(txtMotorAddress.Text))
{
MessageBox.Show("请填写完整的电机信息");
return;
}
_viewModel.LoadDataList.Add(new ChartData
{
Name = txtMotorName.Text,
Address = txtMotorAddress.Text,
Value = 0
});
txtMotorName.Text = "";
txtMotorAddress.Text = "";
}
// 窗口关闭时清理资源
private void Window_Closed(object sender, EventArgs e)
{
if (_isConnected)
{
_plc.Close();
_readTimer.Stop();
// 关闭文件流(添加空值检查)
_rhythmDataWriter?.Close();
_loadDataWriter?.Close();
AddLog("数据文件已关闭");
}
// 停止所有定时器
_logClearTimer?.Stop();
_heartbeatTimer?.Stop();
}
}
}上面代码运行后报以下错误:加载历史数据出错: The process cannot access the file 'D:\PLC_Monitor_Data\rhythm_20251004.csv' because it is being used by another process.