Files
huadongmocaceshiyi/MainWindow.xaml.cs
2026-03-26 10:50:18 +08:00

795 lines
31 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Modbus.Device;
using Sunny.UI;
using System;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using ;
namespace PLCDataMonitor
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
// 数据属性
private Brush _statusColor;
private string _connectionStatus;
string _plcIpAddress = string.Empty;
int _plcPort = 502;
private bool _isConnected;
private Thread _readThread;
private bool _keepReading;
private Function ma;
private DataChange c = new DataChange();
// 关键修改:创建一个共享的线程安全数据队列
private System.Collections.Concurrent.ConcurrentQueue<Tuple<DateTime, double, double>> _sharedCurveDataQueue = new System.Collections.Concurrent.ConcurrentQueue<Tuple<DateTime, double, double>>();
// 页面实例
private HomePage _homePage;
private ReportPage _reportPage;
private CurvePage _curvePage;
// 属性更改通知事件
public event PropertyChangedEventHandler PropertyChanged;
private TcpClient _tcpClient;
private IModbusMaster _modbusMaster;
// 暂停控制HR500.04
private const int PauseControlCoil = 41204; // 写入 true=暂停false=继续
private const int PauseStatusCoil = 41204; // 读取当前暂停状态(同一地址)
// 记录暂停时间段(用于导出过滤)
private List<(DateTime start, DateTime end)> _pausePeriods = new List<(DateTime, DateTime)>();
private bool _lastPausedState = false; // 上一次读取的暂停状态
private DateTime _pauseStartTime; // 本次暂停开始时间
public List<(DateTime start, DateTime end)> PausePeriods => _pausePeriods;
// 【新增】用于记录净运行时间的秒表
private System.Diagnostics.Stopwatch _testStopwatch = new System.Diagnostics.Stopwatch();
// 【新增】用于检测新一轮测试开始
private int _lastActualCount = 0;
private int _lastSetCount = 0;
// 添加清空方法
public void ClearPausePeriods()
{
_pausePeriods.Clear();
_lastPausedState = false;
_pauseStartTime = default;
}
// 连接状态属性
public Brush StatusColor
{
get { return _statusColor; }
set { _statusColor = value; OnPropertyChanged(nameof(StatusColor)); }
}
public string ConnectionStatus
{
get { return _connectionStatus; }
set { _connectionStatus = value; OnPropertyChanged(nameof(ConnectionStatus)); }
}
// 报表相关:记录是否已插入当前批次报表(避免重复插入)
public bool _hasInsertedReport = false;
public MainWindow()
{
InitializeComponent();
DataContext = this;
// 初始化状态
StatusColor = Brushes.Red;
ConnectionStatus = "未连接";
_homePage = new HomePage();
// 订阅暂停/继续事件
_homePage.PauseRequested += (s, e) => SendPauseCommand();
_homePage.ResumeRequested += (s, e) => SendResumeCommand();
// 关键修改:创建报表页时传入共享队列
_reportPage = new ReportPage();
_reportPage.CurveDataQueue = _sharedCurveDataQueue;
// 关键修改:创建曲线页时传入同一个共享队列
_curvePage = new CurvePage(_sharedCurveDataQueue);
// 默认显示数据监控页
MainContentFrame.Navigate(_homePage);
SetupLogging();
_reportPage.OnExportCompleted += () =>
{
_hasInsertedReport = false; // 重置插入标记
};
// 设置IP和端口显示
string ip = ConfigurationManager.AppSettings["PLC_IP"];
string port = ConfigurationManager.AppSettings["PLC_PORT"];
TxtPLCIPValue.Text = string.IsNullOrEmpty(ip) ? "192.168.1.170" : ip;
TxtPLCPortValue.Text = string.IsNullOrEmpty(port) ? "502" : port;
}
public void SetupLogging()
{
var logFilePath = "log.txt";
var fileListener = new TextWriterTraceListener(File.CreateText(logFilePath));
Trace.Listeners.Add(fileListener);
Trace.AutoFlush = true;
}
private void InitializeModbusTcp()
{
try
{
_tcpClient = new TcpClient();
_plcIpAddress = ConfigurationManager.AppSettings["PLC_IP"];
_plcPort = ConfigurationManager.AppSettings["PLC_PORT"].ToString().ToInt16();
var connectResult = _tcpClient.BeginConnect(_plcIpAddress, _plcPort, null, null);
var success = connectResult.AsyncWaitHandle.WaitOne(3000);
if (!success || !_tcpClient.Connected)
throw new Exception("连接PLC超时请检查IP和端口");
_modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
_modbusMaster.Transport.ReadTimeout = 1000;
_modbusMaster.Transport.WriteTimeout = 1000;
ma = new Function(_modbusMaster);
}
catch (Exception ex)
{
throw new Exception($"Modbus初始化失败{ex.Message}");
}
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void BtnHome_Click(object sender, RoutedEventArgs e)
{
UpdateNavButtonStyle(BtnHome, BtnReport, BtnCurve);
MainContentFrame.Navigate(_homePage);
}
private void BtnReport_Click(object sender, RoutedEventArgs e)
{
UpdateNavButtonStyle(BtnReport, BtnHome, BtnCurve);
MainContentFrame.Navigate(_reportPage);
}
private void BtnCurve_Click(object sender, RoutedEventArgs e)
{
UpdateNavButtonStyle(BtnCurve, BtnHome, BtnReport);
MainContentFrame.Navigate(_curvePage);
}
private void UpdateNavButtonStyle(FrameworkElement activeBtn,
params FrameworkElement[] inactiveBtns)
{
if (activeBtn is Button btnActive)
{
btnActive.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#3498DB"));
}
foreach (var btn in inactiveBtns)
{
if (btn is Button btnInactive)
{
btnInactive.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2C3E50"));
}
}
}
private void ConnectToPLC()
{
try
{
Trace.WriteLine("Initializing Modbus TCP connection...");
InitializeModbusTcp();
_isConnected = true;
StatusColor = Brushes.Green;
ConnectionStatus = "已连接";
Trace.WriteLine("Connection established successfully.");
StartReadingData();
}
catch (Exception ex)
{
Trace.WriteLine($"Connection failed: {ex.Message}");
Trace.WriteLine($"Stack Trace: {ex.StackTrace}");
MessageBox.Show($"连接失败:{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
StatusColor = Brushes.Orange;
ConnectionStatus = "连接失败";
}
}
private void DisconnectFromPLC()
{
try
{
_keepReading = false;
// 等待线程结束,最多 2 秒
if (_readThread != null && _readThread.IsAlive)
{
if (!_readThread.Join(2000))
{
// 如果线程未结束,强制终止(慎用)
// _readThread.Abort(); // 不推荐,此处仅为保底
}
}
_modbusMaster?.Dispose();
_modbusMaster = null;
if (_tcpClient != null)
{
if (_tcpClient.Connected)
_tcpClient.Close();
_tcpClient.Dispose();
_tcpClient = null;
}
_isConnected = false;
StatusColor = Brushes.Red;
ConnectionStatus = "已断开";
_homePage.UpdateData("0.0", "0.0", "0.0", "0.0", 0, 0, "0", "0", "0");
}
catch (Exception ex)
{
MessageBox.Show($"断开失败:{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void StartReadingData()
{
_keepReading = true;
_readThread = new Thread(ReadDataLoop) { IsBackground = true };
_readThread.Start();
}
private void ReadDataLoop()
{
//int i = 0;
//while (i < 100)
//{
// // 添加随机测试数据
// Random random = new Random();
// // 生成随机摩擦力值(包含负值)
// double friction1 = random.Next(-200, 200);
// double friction2 = random.Next(-200, 200);
// // 创建数据点(时间戳相同,确保数据一致性)
// var dataPoint = new Tuple<DateTime, double, double>(
// DateTime.Now,
// friction1,
// friction2
// );
// // 关键修改:同时添加到两个地方
// // 1. 添加到共享曲线数据队列(曲线页面和报表页面共享)
// _sharedCurveDataQueue.Enqueue(dataPoint);
// // 2. 添加到报表数据
// _reportPage.AddReportRecord(new ReportData
// {
// D = random.Next(1, 100),
// Friction1 = friction1.ToString("F1"), // 使用相同的摩擦力值
// Friction2 = friction2.ToString("F1"), // 使用相同的摩擦力值
// Id = (i + 11).ToString(),
// Pressure1 = random.Next(1, 10).ToString(),
// Pressure2 = random.Next(2, 12).ToString(),
// t1 = random.Next(20, 40),
// t2 = random.Next(25, 45),
// Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") // 使用相同的时间格式
// });
// // 3. 通知曲线页面更新(这个调用只是触发重绘,数据已经在队列中)
// _curvePage.AddFrictionData(friction1, friction2);
// // 为了让曲线更平滑,可以少量数据生成
// Thread.Sleep(100); // 每100ms一个数据点
// i++;
//}
while (_keepReading)
{
try
{
if (_isConnected && _tcpClient != null && _tcpClient.Connected)
{
// 读取所有数据(包含新增的次数、最大摩擦力)
var data = ReadModbusData();
// 读取暂停状态
bool isPaused = false;
try
{
bool[] pauseStatus = _modbusMaster?.ReadCoils(0x01, (ushort)PauseStatusCoil, 1);
isPaused = pauseStatus != null && pauseStatus.Length > 0 && pauseStatus[0];
}
catch
{
// 读取失败时默认 false
}
// --- 记录暂停时间段 ---
if (isPaused != _lastPausedState)
{
if (isPaused) // 进入暂停
{
_pauseStartTime = DateTime.Now;
}
else // 退出暂停
{
if (_pauseStartTime != default)
{
_pausePeriods.Add((_pauseStartTime, DateTime.Now));
}
}
_lastPausedState = isPaused;
// 【新增】同步更新 HomePage 上的按钮状态
Dispatcher.BeginInvoke(new Action(() =>
{
_homePage.SetButtonState(isPaused);
}), System.Windows.Threading.DispatcherPriority.Background);
}
// 【核心修改】1. 控制秒表逻辑 (处理暂停和重新开始)
// 判断是否是新的一轮测试开始 (实际次数归1或者从上一轮结束状态变为1)
bool isNewTestStart = (data.actualCount == 1 && _lastActualCount == 0) ||
(data.actualCount == 1 && _lastActualCount == _lastSetCount && _lastSetCount > 0);
if (isNewTestStart)
{
// 新一轮测试,重置并启动秒表
_testStopwatch.Restart();
}
else
{
// 非新一轮,根据暂停状态控制秒表
if (!isPaused && data.setCount > 0 && data.actualCount > 0)
{
// 测试进行中且未暂停 -> 启动秒表 (如果之前是停的)
if (!_testStopwatch.IsRunning) _testStopwatch.Start();
}
else
{
// 暂停中 或 测试未开始 -> 停止秒表
if (_testStopwatch.IsRunning) _testStopwatch.Stop();
}
}
// 更新记录,供下一次循环判断
_lastActualCount = data.actualCount;
_lastSetCount = data.setCount;
// 【核心修改】2. 生成“净运行时间”戳
// 使用 DateTime.MinValue 作为基准,加上秒表时间。
// 这样计算 TotalSeconds 时,得到的就是纯粹的秒数,不包含暂停时间。
DateTime effectiveTime = DateTime.MinValue.AddMilliseconds(_testStopwatch.ElapsedMilliseconds);
// --- 结束记录暂停时间段 ---
var pressure1 = data.pressure1;
var pressure2 = data.pressure2;
var friction1 = data.friction1;
var friction2 = data.friction2;
var maxFriction1 = data.maxFriction1;
var maxFriction2 = data.maxFriction2;
// ----- 读取报警状态HR504 / HR509-----
bool[] alarmsHR504 = null;
bool[] alarmsHR509 = null;
try
{
// 连续读取 HR504.1 ~ HR504.55个线圈起始 41265
alarmsHR504 = _modbusMaster?.ReadCoils(0x01, 41265, 5);
// 连续读取 HR509.0 ~ HR509.34个线圈起始 41345
alarmsHR509 = _modbusMaster?.ReadCoils(0x01, 41344, 6);
}
catch
{
}
string alarmMessage = "";
if (alarmsHR504 != null && alarmsHR504.Length >= 5)
{
// 根据实测索引0=41265=已达上限水位
if (alarmsHR504[0]) alarmMessage += "已达上限水位 ";
// 索引1=41266=急停被按下
if (alarmsHR504[1]) alarmMessage += "急停被按下 ";
// 索引2=41267=测试完成(待确认)
if (alarmsHR504[2]) alarmMessage += "测试完成 ";
// 索引3=41268=摩擦力过大(待确认)
//if (alarmsHR504[3]) alarmMessage += "摩擦力过大 ";
// 索引4=41269=超温95℃待确认
if (alarmsHR504[4]) alarmMessage += "超温95℃ ";
}
if (alarmsHR509 != null && alarmsHR509.Length >= 6)
{
if (alarmsHR509[0]) alarmMessage += "1工位低水位 ";
if (alarmsHR509[2]) alarmMessage += "水泵未运行 ";
// 索引3=41348=2工位低水位
if (alarmsHR509[3]) alarmMessage += "2工位低水位 ";
if (alarmsHR509[4]) alarmMessage += "摩擦力过大 ";
if (alarmsHR509[5]) alarmMessage += "伺服驱动器报警,请清除 ";
}
// 更新UI数据监控页+曲线页)
// 使用 BeginInvoke 避免阻塞后台读取线程,减少 UI 卡死风险
Dispatcher.BeginInvoke(new Action(() =>
{
// 更新数据监控页数值
_homePage.UpdateData(
pressure1.ToString("F1"),
pressure2.ToString("F1"),
friction1.ToString("F1"),
friction2.ToString("F1"),
data.setCount,
data.actualCount,
data.t1.ToString("F1"),
data.t2.ToString("F1"),
data.D.ToString("F1")
);
// 新增:更新报警显示
_homePage.UpdateAlarms(alarmMessage);
if (!isPaused)
{
// 关键修改:确保曲线数据正确记录
if (data.setCount > 0 && data.actualCount > 0 &&
data.setCount > data.actualCount) // 点击了开始,且实际次数小于设定次数,才更新曲线
{
// 创建数据点
var dataPoint = new Tuple<DateTime, double, double>(
effectiveTime,
friction1,
friction2
);
// 添加到共享队列
_sharedCurveDataQueue.Enqueue(dataPoint);
// 通知曲线页面更新
_curvePage.AddFrictionData(friction1, friction2);
}
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
//);
// 获取当前的净运行秒数
double currentElapsedSec = _testStopwatch.ElapsedMilliseconds / 1000.0;
var reportRecord = new ReportData(
currentElapsedSec.ToString(), // 绝对时间可以保留作为“发生时刻”,或者改为显示秒数
pressure1.ToString("F1"),
pressure2.ToString("F1"),
maxFriction1.ToString("F1"),
maxFriction2.ToString("F1"),
data.t1,
data.t2,
data.D
// 注意:如果你的 ReportData 构造函数没有 ElapsedSeconds 参数,需要重载构造函数或直接赋值
);
reportRecord.ElapsedSeconds = currentElapsedSec;
// 插入报表
_reportPage.AddReportRecord(reportRecord);
// 标记为已插入(避免同一批次重复插入)
_hasInsertedReport = true;
}
if (data.actualCount < data.setCount)
{
_hasInsertedReport = false;
}
}
}), System.Windows.Threading.DispatcherPriority.Background);
}
else
{
// 如果连接标志为 true 但实际未连接,主动断开
if (_isConnected)
{
Dispatcher.BeginInvoke(new Action(() => DisconnectFromPLC()));
}
}
}
catch (Exception ex)
{
// 捕获 Socket 异常
if (ex is SocketException || (ex.InnerException is SocketException) ||
ex.Message.Contains("non-connected") || ex.Message.Contains("socket"))
{
// 连接已丢失,触发断开
Dispatcher.BeginInvoke(new Action(() => DisconnectFromPLC()));
}
else
{
// 其他异常正常显示
Dispatcher.BeginInvoke(new Action(() =>
{
StatusColor = Brushes.Orange;
ConnectionStatus = $"读取错误:{ex.Message}";
}), System.Windows.Threading.DispatcherPriority.Background);
}
}
Thread.Sleep(200); // 100ms刷新一次
}
}
public void ResetTestTimer()
{
if (_testStopwatch != null)
{
_testStopwatch.Reset();
}
// 同时也重置一下计数记录,防止逻辑判断错误
_lastActualCount = 0;
_lastSetCount = 0;
}
private (float pressure1, float pressure2, float friction1, float friction2,
int setCount, int actualCount, float maxFriction1, float maxFriction2, float t1, float t2, float D, bool status) ReadModbusData()
{
try
{
// 修正地址按手册公式计算后的Modbus地址
ushort[] p1 = _modbusMaster?.ReadHoldingRegisters(0x01, 18701, 2); // 原DM170017001+1700=18701
ushort[] p2 = _modbusMaster?.ReadHoldingRegisters(0x01, 18705, 2); // 原DM170417001+1704=18705
ushort[] f1 = _modbusMaster?.ReadHoldingRegisters(0x01, 17045, 2); // 原DM4417001+44=17045
ushort[] f2 = _modbusMaster?.ReadHoldingRegisters(0x01, 17085, 2); // 原DM8417001+84=17085
// 设定次数H64
ushort[] setCountArr = _modbusMaster?.ReadHoldingRegisters(0x01, 6734, 1);
ushort[] actualCountArr = _modbusMaster?.ReadHoldingRegisters(0x01, 19000, 1);
// 最大摩擦力1DM160017001+1600=18601
ushort[] maxF1Arr = _modbusMaster?.ReadHoldingRegisters(0x01, 18601, 2);
// 最大摩擦力2DM160417001+1604=18605
ushort[] maxF2Arr = _modbusMaster?.ReadHoldingRegisters(0x01, 18605, 2);
// 温度和其他数据
ushort[] p11 = _modbusMaster?.ReadHoldingRegisters(0x01, 18201, 2);
ushort[] p12 = _modbusMaster?.ReadHoldingRegisters(0x01, 18205, 2);
ushort[] p13 = _modbusMaster?.ReadHoldingRegisters(0x01, 19101, 2);
bool[] p14 = _modbusMaster?.ReadCoils(0x01, 7100, 100);
if (p14[0])
{
}
// 转换数据压力、实时摩擦力float类型
float pressure1 = c.UshortToFloat(p1[0], p1[1]);
float pressure2 = c.UshortToFloat(p2[0], p2[1]);
float friction1 = c.UshortToFloat(f1[0], f1[1]);
float friction2 = c.UshortToFloat(f2[0], f2[1]);
float pressure11 = c.UshortToFloat(p11[0], p11[1]);
float pressure12 = c.UshortToFloat(p12[0], p12[1]);
float pressure13 = c.UshortToFloat(p13[0], p13[1]);
// 转换数据设定次数、实际次数int类型
int setCount = setCountArr[0]; // 16位寄存器直接转int
int actualCount = actualCountArr[0];
// 转换数据最大摩擦力float类型
float maxFriction1 = c.UshortToFloat(maxF1Arr[0], maxF1Arr[1]);
float maxFriction2 = c.UshortToFloat(maxF2Arr[0], maxF2Arr[1]);
bool stauts = p14[0];
// 保留4位小数返回
return (
(float)Math.Round(pressure1, 4),
(float)Math.Round(pressure2, 4),
(float)Math.Round(friction1, 4),
(float)Math.Round(friction2, 4),
setCount,
actualCount,
maxFriction1,
maxFriction2,
pressure11,
pressure12,
pressure13,
stauts
);
}
catch (Exception ex)
{
//throw new Exception($"Modbus读取错误{ex.Message}");
return (
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
true
);
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
DisconnectFromPLC();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
//if (!_isConnected)
//{
// ConnectToPLC();
// if (_isConnected)
// {
// BtnConnectPLC.Content = "断开PLC";
// BtnConnectPLC.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E67E22"));
// }
//}
//else
//{
// DisconnectFromPLC();
// BtnConnectPLC.Content = "连接PLC";
// BtnConnectPLC.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27AE60"));
//}
// 只做界面初始化,不自动连接
// 确保连接按钮状态正确
BtnConnectPLC.Content = "连接PLC";
BtnConnectPLC.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27AE60"));
StatusColor = Brushes.Red;
ConnectionStatus = "未连接";
}
private void ConnectButton_Click(object sender, RoutedEventArgs e)
{
if (!_isConnected)
{
ConnectToPLC();
// 更新按钮文本
BtnConnectPLC.Content = "断开PLC";
BtnConnectPLC.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E67E22"));
}
else
{
DisconnectFromPLC();
BtnConnectPLC.Content = "连接PLC";
BtnConnectPLC.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27AE60"));
}
}
private void SendPauseCommand()
{
try
{
if (_isConnected && _modbusMaster != null)
{
_modbusMaster.WriteSingleCoil(0x01, (ushort)PauseControlCoil, true);
Thread.Sleep(100);
}
}
catch (Exception ex)
{
MessageBox.Show($"发送暂停命令失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void SendResumeCommand()
{
try
{
if (_isConnected && _modbusMaster != null)
{
_modbusMaster.WriteSingleCoil(0x01, (ushort)PauseControlCoil, false);
Thread.Sleep(100);
}
}
catch (Exception ex)
{
MessageBox.Show($"发送继续命令失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}