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> _sharedCurveDataQueue = new System.Collections.Concurrent.ConcurrentQueue>(); // 页面实例 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.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.5(5个线圈,起始 41265) alarmsHR504 = _modbusMaster?.ReadCoils(0x01, 41265, 5); // 连续读取 HR509.0 ~ HR509.3(4个线圈,起始 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( 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); // 原DM1700,17001+1700=18701 ushort[] p2 = _modbusMaster?.ReadHoldingRegisters(0x01, 18705, 2); // 原DM1704,17001+1704=18705 ushort[] f1 = _modbusMaster?.ReadHoldingRegisters(0x01, 17045, 2); // 原DM44,17001+44=17045 ushort[] f2 = _modbusMaster?.ReadHoldingRegisters(0x01, 17085, 2); // 原DM84,17001+84=17085 // 设定次数(H64) ushort[] setCountArr = _modbusMaster?.ReadHoldingRegisters(0x01, 6734, 1); ushort[] actualCountArr = _modbusMaster?.ReadHoldingRegisters(0x01, 19000, 1); // 最大摩擦力1(DM1600):17001+1600=18601 ushort[] maxF1Arr = _modbusMaster?.ReadHoldingRegisters(0x01, 18601, 2); // 最大摩擦力2(DM1604):17001+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); } } } }