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;
using System.Linq;
using System.Collections.Generic;
using System.Windows.Input;
using System.Collections.Concurrent;
using System.Diagnostics;
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 Dictionary<string, bool> _rhythmAlarmStatus = new Dictionary<string, bool>();
private Dictionary<string, bool> _loadAlarmStatus = new Dictionary<string, bool>();
private bool _rhythmNeedsRedraw = false;
private bool _loadNeedsRedraw = false;
private DispatcherTimer _renderTimer = new DispatcherTimer();
public MainWindow()
{
InitializeComponent();
_viewModel = new PlcMonitorViewModel();
DataContext = _viewModel;
_plc = new ActUtlType64();
_readTimer = new DispatcherTimer();
_logClearTimer = new DispatcherTimer();
_heartbeatTimer = new DispatcherTimer(); // 初始化心跳定时器
InitializePlc();
InitializeLogClearTimer();
InitializeHeartbeatTimer(); // 初始化心跳定时器
InitializeDataSaving(); //数据保存
InitializeDefaultDates(); // 初始化默认日期时间
InitializeRenderTimer(); // 新增渲染定时器初始化
}
private void InitializeRenderTimer()
{
// 使用已创建的实例进行配置
_renderTimer.Interval = TimeSpan.FromMilliseconds(300);
_renderTimer.Tick += RenderTimer_Tick;
_renderTimer.Start();
AddLog("图表渲染定时器已启动");
}
// 修改渲染处理方法
private void RenderTimer_Tick(object? sender, EventArgs e)
{
// 添加空值检查确保安全
if (_renderTimer == null) return;
// 按需渲染图表
if (_rhythmNeedsRedraw)
{
DrawBarChart(canvasRhythmChart, _viewModel.RhythmDataList, true, "rhythm");
_rhythmNeedsRedraw = false;
}
if (_loadNeedsRedraw)
{
DrawBarChart(canvasLoadChart, _viewModel.LoadDataList, true, "load");
_loadNeedsRedraw = false;
}
}
private void InitializeDefaultDates()
{
dpStartDate.SelectedDate = DateTime.Today;
dpEndDate.SelectedDate = DateTime.Today;
txtStartTime.Text = "00:00:00";
txtEndTime.Text = "23:59:59";
}
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 || _rhythmDataWriter.BaseStream == null)
{
return;
}
// 写入CSV数据
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
string csvLine = $"{timestamp},{data.Name},{data.Address},{value}";
// 使用线程安全的方式写入
lock (_rhythmDataWriter)
{
_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 || _loadDataWriter.BaseStream == null)
{
return;
}
// 写入CSV数据
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
string csvLine = $"{timestamp},{data.Name},{data.Address},{value}";
// 使用线程安全的方式写入
lock (_loadDataWriter)
{
_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数据");
}
// 重构异步读取逻辑
private async Task ReadPlcDataAsync()
{
if (!_isConnected) return;
try
{
// 并行读取三类数据
var tasks = new List<Task>
{
Task.Run(ReadMotorData),
Task.Run(ReadRhythmData),
Task.Run(ReadLoadData)
};
await Task.WhenAll(tasks);
}
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);
bool valueChanged = false;
string? logMessage = null;
if (result == 0)
{
float actualValue = value;
var data = _viewModel.MotorDataList[i];
// 检查值是否发生变化
if (_lastMotorValues.Count <= i)
{
_lastMotorValues.Add(actualValue);
valueChanged = true;
if (actualValue != data.StandardValue)
{
logMessage = $"点位 {data.PointName} 异常: 实际值 {actualValue} ≠ 标准值 {data.StandardValue}";
}
}
else if (_lastMotorValues[i] != actualValue)
{
_lastMotorValues[i] = actualValue;
valueChanged = true;
if (actualValue == data.StandardValue)
{
logMessage = $"点位 {data.PointName} 恢复正常";
}
else
{
logMessage = $"点位 {data.PointName} 异常: 实际值 {actualValue} ≠ 标准值 {data.StandardValue}";
}
}
// 只在值变化时更新UI
if (valueChanged)
{
Application.Current.Dispatcher.Invoke(() =>
{
data.ActualValue = actualValue;
data.Status = (actualValue == data.StandardValue) ? "正常" : "异常";
});
if (!string.IsNullOrEmpty(logMessage))
{
AddLog(logMessage);
}
}
}
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;
var data = _viewModel.RhythmDataList[i];
bool valueChanged = Math.Abs(data.Value - rhythmValue) > 0.01f;
if (valueChanged)
{
// 更新UI线程中的值
Application.Current.Dispatcher.Invoke(() =>
{
data.Value = rhythmValue;
_rhythmNeedsRedraw = true;
});
// 检查并记录报警
CheckRhythmAlarm(data, rhythmValue);
// 异步保存数据
Task.Run(() => SaveRhythmData(data, rhythmValue));
}
}
else
{
AddLog($"读取节拍数据 {_viewModel.RhythmDataList[i].Name} 失败,错误代码: {result}");
}
}
}
// 检查节拍时间报警
private void CheckRhythmAlarm(ChartData data, float value)
{
if (string.IsNullOrEmpty(data.Address)) return;
string address = data.Address;
// 初始化报警状态
if (!_rhythmAlarmStatus.ContainsKey(address))
{
_rhythmAlarmStatus[address] = false; // 初始状态为正常
}
bool isAlarm = value > 30; // 超过30秒为异常
// 状态发生变化时记录日志
if (_rhythmAlarmStatus[address] != isAlarm)
{
string name = data.Name ?? "未知部位";
if (isAlarm)
{
AddLog($"【节拍时间报警】部位 {name} ({address}) 节拍时间 {value:F1}s 超出正常范围(>30s)");
}
else
{
AddLog($"【节拍时间恢复】部位 {name} ({address}) 节拍时间 {value:F1}s 已恢复正常(≤30s)");
}
// 更新状态
_rhythmAlarmStatus[address] = isAlarm;
}
}
// 优化负载数据读取
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;
var data = _viewModel.LoadDataList[i];
bool valueChanged = Math.Abs(data.Value - loadValue) > 0.01f;
if (valueChanged)
{
// 更新UI线程中的值
Application.Current.Dispatcher.Invoke(() =>
{
data.Value = loadValue;
_loadNeedsRedraw = true;
});
// 检查并记录报警
CheckLoadAlarm(data, loadValue);
// 异步保存数据
Task.Run(() => SaveLoadData(data, loadValue));
}
}
else
{
AddLog($"读取负载数据 {_viewModel.LoadDataList[i].Name} 失败,错误代码: {result}");
}
}
}
// 检查负载率报警
private void CheckLoadAlarm(ChartData data, float value)
{
if (string.IsNullOrEmpty(data.Address)) return;
string address = data.Address;
// 初始化报警状态
if (!_loadAlarmStatus.ContainsKey(address))
{
_loadAlarmStatus[address] = false; // 初始状态为正常
}
bool isAlarm = value >= 80; // 超过80%为异常
// 状态发生变化时记录日志
if (_loadAlarmStatus[address] != isAlarm)
{
string name = data.Name ?? "未知电机";
if (isAlarm)
{
AddLog($"【负载率报警】电机 {name} ({address}) 负载率 {value:F1}% 超出正常范围(≥80%)");
}
else
{
AddLog($"【负载率恢复】电机 {name} ({address}) 负载率 {value:F1}% 已恢复正常(<80%)");
}
// 更新状态
_loadAlarmStatus[address] = isAlarm;
}
}
// 绘制柱状图
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 async void LoadHistoryData_Click(object sender, RoutedEventArgs e)
{
// 保存原始按钮内容
var originalContent = btnLoadHistory.Content;
// 创建进度报告器
var progress = new Progress<int>(percent =>
{
progressBar.Value = percent;
});
//// 在开始加载历史数据时暂停监控
_readTimer?.Stop();
_renderTimer?.Stop(); // 暂停图表渲染
try
{
// 安全获取 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 (dpStartDate.SelectedDate == null || dpEndDate.SelectedDate == null)
{
MessageBox.Show("请选择开始日期和结束日期");
return;
}
// 解析时间
DateTime startDate = dpStartDate.SelectedDate.Value;
DateTime endDate = dpEndDate.SelectedDate.Value;
if (!TimeSpan.TryParseExact(txtStartTime.Text, "hh\\:mm\\:ss",
CultureInfo.InvariantCulture, out TimeSpan startTime))
{
MessageBox.Show("开始时间格式不正确,请使用HH:mm:ss格式");
return;
}
if (!TimeSpan.TryParseExact(txtEndTime.Text, "hh\\:mm\\:ss",
CultureInfo.InvariantCulture, out TimeSpan endTime))
{
MessageBox.Show("结束时间格式不正确,请使用HH:mm:ss格式");
return;
}
DateTime startDateTime = startDate.Date + startTime;
DateTime endDateTime = endDate.Date + endTime;
if (startDateTime >= endDateTime)
{
MessageBox.Show("开始时间必须早于结束时间");
return;
}
// 禁用按钮并显示加载状态
btnLoadHistory.Content = "加载中...";
btnLoadHistory.IsEnabled = false;
// 显示进度条
progressBar.Visibility = Visibility.Visible;
progressBar.Value = 0;
Cursor = Cursors.Wait;
// 添加资源优化提示
AddLog("开始加载历史数据,监控功能将临时暂停...");
await LoadAndDrawHistoryDataAsync(dataType, startDateTime, endDateTime, progress);
}
catch (Exception ex)
{
MessageBox.Show($"加载历史数据失败: {ex.Message}");
AddLog($"加载历史数据出错: {ex.Message}");
}
finally
{
// 恢复UI状态和监控功能
btnLoadHistory.Content = originalContent;
btnLoadHistory.IsEnabled = true;
progressBar.Visibility = Visibility.Collapsed;
Cursor = Cursors.Arrow;
// 重新启动监控
if (_isConnected)
{
_readTimer?.Start();
_renderTimer?.Start();
AddLog("历史数据加载完成,监控功能已恢复");
}
}
}
private async Task LoadAndDrawHistoryDataAsync(string dataType, DateTime startTime, DateTime endTime, IProgress<int> progress)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
canvasHistoryChart.Children.Clear();
AddLog($"开始加载{dataType}历史数据 ({startTime:yyyy-MM-dd HH:mm:ss} 到 {endTime:yyyy-MM-dd HH:mm:ss})");
// 获取文件列表
var files = GetDataFilesInRange(dataType, startTime, endTime);
AddLog($"找到 {files.Count} 个数据文件");
if (files.Count == 0)
{
MessageBox.Show($"在指定时间段内找不到{dataType}的历史数据文件");
return;
}
// 计算平均值
var averagedData = await Task.Run(() =>
CalculateAveragesAsync(files, startTime, endTime, dataType, progress));
AddLog($"数据处理完成,共 {averagedData.Count} 个点位,耗时 {stopwatch.Elapsed.TotalSeconds:F2} 秒");
if (averagedData.Count == 0)
{
MessageBox.Show("在指定时间段内没有找到有效数据");
return;
}
// 绘制图表
await Dispatcher.InvokeAsync(() =>
{
DrawHistoryBarChart(averagedData, dataType, startTime, endTime);
AddLog($"图表绘制完成,总耗时 {stopwatch.Elapsed.TotalSeconds:F2} 秒");
});
}
catch (Exception ex)
{
AddLog($"加载历史数据出错: {ex.Message},总耗时 {stopwatch.Elapsed.TotalSeconds:F2} 秒");
throw;
}
finally
{
stopwatch.Stop();
}
}
// 获取时间段内的数据文件
private List<string> GetDataFilesInRange(string dataType, DateTime startTime, DateTime endTime)
{
var files = new List<string>();
string filePrefix = dataType == "节拍时间" ? "rhythm" : "load";
for (DateTime date = startTime.Date; date <= endTime.Date; date = date.AddDays(1))
{
string fileName = $"{filePrefix}_{date:yyyyMMdd}.csv";
string filePath = Path.Combine(_saveDirectory, fileName);
if (File.Exists(filePath))
{
files.Add(filePath);
}
}
return files;
}
// 计算每个点位的平均值 - 添加进度报告
// 改进的文件读取方法,减少内存分配和磁盘I/O
private Dictionary<string, double> CalculateAveragesAsync(
List<string> files, DateTime startTime, DateTime endTime,
string dataType, IProgress<int> progress)
{
var pointData = new ConcurrentDictionary<string, (double Sum, int Count)>();
int totalFiles = files.Count;
int processedFiles = 0;
// 限制并行度,避免过度竞争资源
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) // 减少并行度
};
Parallel.ForEach(files, parallelOptions, filePath =>
{
try
{
// 使用更高效的文件读取方式
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(fs, Encoding.UTF8, false, 8192); // 增加缓冲区大小
// 跳过标题行
reader.ReadLine();
string? line;
while ((line = reader.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(',', 4); // 限制分割数量,提高性能
if (parts.Length < 4) continue;
// 解析时间戳和数值
if (DateTime.TryParseExact(parts[0], "yyyy-MM-dd HH:mm:ss.fff",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime timestamp) &&
double.TryParse(parts[3], out double value))
{
// 检查时间是否在指定范围内
if (timestamp >= startTime && timestamp <= endTime)
{
string pointName = parts[1];
// 使用元组减少内存分配
pointData.AddOrUpdate(
pointName,
(value, 1),
(key, oldValue) => (oldValue.Sum + value, oldValue.Count + 1));
}
}
}
}
catch (Exception ex)
{
// 记录错误但继续处理其他文件
System.Diagnostics.Debug.WriteLine($"读取文件 {filePath} 时出错: {ex.Message}");
}
finally
{
// 更新进度
int newProcessed = Interlocked.Increment(ref processedFiles);
int percent = (int)((double)newProcessed / totalFiles * 100);
progress?.Report(percent);
}
});
// 计算平均值
var averages = new ConcurrentDictionary<string, double>();
Parallel.ForEach(pointData, kvp =>
{
if (kvp.Value.Count > 0)
{
averages[kvp.Key] = kvp.Value.Sum / kvp.Value.Count;
}
});
return new Dictionary<string, double>(averages);
}
// 绘制历史数据柱形图
// 改进的图表渲染方法,减少UI操作
private void DrawHistoryBarChart(Dictionary<string, double> data, string dataType, DateTime startTime, DateTime endTime)
{
canvasHistoryChart.Children.Clear();
if (data.Count == 0)
{
// 绘制空状态提示
var emptyText = new TextBlock
{
Text = "没有找到符合条件的数据",
FontSize = 14,
Foreground = Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
Canvas.SetLeft(emptyText, canvasHistoryChart.ActualWidth / 2 - 70);
Canvas.SetTop(emptyText, canvasHistoryChart.ActualHeight / 2 - 10);
canvasHistoryChart.Children.Add(emptyText);
return;
}
// 使用更简单的绘制方式,避免复杂的Visual树操作
double canvasWidth = canvasHistoryChart.ActualWidth;
double canvasHeight = canvasHistoryChart.ActualHeight;
if (canvasWidth <= 0 || canvasHeight <= 0) return;
const double margin = 40;
double availableWidth = canvasWidth - 2 * margin;
double availableHeight = canvasHeight - 2 * margin;
// 限制显示的点位数量,避免过度渲染
var displayData = data.Take(50).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (data.Count > 50)
{
AddLog($"数据量较大,仅显示前50个点位(共{data.Count}个)");
}
double barWidth = Math.Max(10, (availableWidth - (displayData.Count - 1) * 5) / displayData.Count);
double maxValue = displayData.Values.Max();
if (maxValue == 0) maxValue = 1;
// 绘制坐标轴
var yAxis = new Line
{
X1 = margin,
Y1 = margin,
X2 = margin,
Y2 = canvasHeight - margin,
Stroke = Brushes.Black,
StrokeThickness = 2
};
canvasHistoryChart.Children.Add(yAxis);
var xAxis = new Line
{
X1 = margin,
Y1 = canvasHeight - margin,
X2 = canvasWidth - margin,
Y2 = canvasHeight - margin,
Stroke = Brushes.Black,
StrokeThickness = 2
};
canvasHistoryChart.Children.Add(xAxis);
// 简化的刻度绘制
for (int i = 0; i <= 5; i++)
{
double y = canvasHeight - margin - (i * availableHeight / 5);
double value = i * maxValue / 5;
var tick = new Line
{
X1 = margin - 5,
Y1 = y,
X2 = margin,
Y2 = y,
Stroke = Brushes.Black,
StrokeThickness = 1
};
canvasHistoryChart.Children.Add(tick);
var tickText = new TextBlock
{
Text = value.ToString("F1"),
FontSize = 9,
Foreground = Brushes.Black
};
Canvas.SetLeft(tickText, margin - 25);
Canvas.SetTop(tickText, y - 8);
canvasHistoryChart.Children.Add(tickText);
}
// 绘制柱形
int index = 0;
foreach (var kvp in displayData)
{
double value = kvp.Value;
double barHeight = (value / maxValue) * availableHeight;
double x = margin + index * (barWidth + 5);
double y = canvasHeight - margin - barHeight;
Brush barColor = GetBarColor(dataType == "节拍时间" ? "rhythm" : "load", value);
var rect = new Rectangle
{
Width = barWidth,
Height = barHeight,
Fill = barColor,
Stroke = Brushes.Black,
StrokeThickness = 1
};
Canvas.SetLeft(rect, x);
Canvas.SetTop(rect, y);
canvasHistoryChart.Children.Add(rect);
// 只在有足够空间时显示数值
if (barWidth > 20)
{
var valueText = new TextBlock
{
Text = value.ToString("F1"),
FontSize = 9,
Foreground = Brushes.Black,
Width = barWidth,
TextAlignment = TextAlignment.Center
};
Canvas.SetLeft(valueText, x);
Canvas.SetTop(valueText, y - 15);
canvasHistoryChart.Children.Add(valueText);
}
// 点位名称(简化显示)
var nameText = new TextBlock
{
Text = kvp.Key.Length > 8 ? kvp.Key.Substring(0, 8) + "..." : kvp.Key,
FontSize = 8,
Foreground = Brushes.Black,
Width = barWidth + 10,
TextAlignment = TextAlignment.Center
};
Canvas.SetLeft(nameText, x - 5);
Canvas.SetTop(nameText, canvasHeight - margin + 5);
canvasHistoryChart.Children.Add(nameText);
index++;
}
// 添加统计信息
var statsText = new TextBlock
{
Text = $"统计: {displayData.Count}个点位 | 最大值: {displayData.Values.Max():F2} | 平均值: {displayData.Values.Average():F2}",
FontSize = 10,
Foreground = Brushes.DarkBlue,
FontWeight = FontWeights.Bold
};
Canvas.SetLeft(statsText, canvasWidth / 2 - 150);
Canvas.SetTop(statsText, 10);
canvasHistoryChart.Children.Add(statsText);
}
// DrawingVisual容器类
private class VisualHost : FrameworkElement
{
private readonly Visual _visual;
public VisualHost(Visual visual)
{
_visual = visual;
AddVisualChild(_visual);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index)
{
if (index != 0)
throw new ArgumentOutOfRangeException(nameof(index));
return _visual;
}
// 移除 MeasureOverride 和 ArrangeOverride 方法
// 因为我们不需要自定义布局逻辑
}
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盘
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();
// 清空节拍报警状态
_rhythmAlarmStatus.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();
// 清空负载报警状态
_loadAlarmStatus.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;
}
var newData = new ChartData
{
Name = txtSectionName.Text,
Address = txtSectionAddress.Text,
Value = 0
};
_viewModel.RhythmDataList.Add(newData);
// 初始化报警状态
_rhythmAlarmStatus[newData.Address] = false;
txtSectionName.Text = "";
txtSectionAddress.Text = "";
}
// 添加负载监控电机
private void AddMotor_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(txtMotorName.Text) ||
string.IsNullOrWhiteSpace(txtMotorAddress.Text))
{
MessageBox.Show("请填写完整的电机信息");
return;
}
var newData = new ChartData
{
Name = txtMotorName.Text,
Address = txtMotorAddress.Text,
Value = 0
};
_viewModel.LoadDataList.Add(newData);
// 初始化报警状态
_loadAlarmStatus[newData.Address] = false;
txtMotorName.Text = "";
txtMotorAddress.Text = "";
}
// 窗口关闭时清理所有资源
private void Window_Closed(object sender, EventArgs e)
{
try
{
// 停止所有定时器
_readTimer?.Stop();
_logClearTimer?.Stop();
_heartbeatTimer?.Stop();
_renderTimer?.Stop(); // 新增渲染定时器停止
// 关闭PLC连接
if (_isConnected)
{
_plc.Close();
}
// 关闭文件流
if (_rhythmDataWriter != null)
{
_rhythmDataWriter.Close();
_rhythmDataWriter.Dispose();
}
if (_loadDataWriter != null)
{
_loadDataWriter.Close();
_loadDataWriter.Dispose();
}
AddLog("应用程序已关闭,资源已释放");
}
catch (Exception ex)
{
MessageBox.Show($"关闭时发生错误: {ex.Message}");
}
}
}
}
上面代码运行后,在加载历史数据时已经停掉了电极电位监控,节拍时间监控,负载率监控,为什么在点击历史数据时电极电位监控还是实时显示数值?
最新发布