using Microsoft.VisualBasic; using PetroleumViscosityTest.Controls; using PetroleumViscosityTest.Models; using System; using System.Collections.Generic; using System.IO.Ports; using System.Linq; using System.Timers; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; namespace PetroleumViscosityTest { public partial class MainWindow : Window { private ViscosityTestData currentData; private List rawTimes = new List(); // --- 新增 Modbus 通信相关字段 --- private SerialPort serialPort; private System.Timers.Timer readTimer; private byte slaveId = 0x01; // 从机地址,根据实际修改 private readonly object portLock = new object(); private System.Diagnostics.Stopwatch stopwatch1 = new System.Diagnostics.Stopwatch(); private System.Diagnostics.Stopwatch stopwatch2 = new System.Diagnostics.Stopwatch(); private System.Diagnostics.Stopwatch stopwatch3 = new System.Diagnostics.Stopwatch(); private System.Diagnostics.Stopwatch stopwatch4 = new System.Diagnostics.Stopwatch(); // 新增:四个 DispatcherTimer,用于实时更新 private DispatcherTimer timer1 = new DispatcherTimer(); private DispatcherTimer timer2 = new DispatcherTimer(); private DispatcherTimer timer3 = new DispatcherTimer(); private DispatcherTimer timer4 = new DispatcherTimer(); // -------------------------------- public MainWindow() { InitializeComponent(); currentData = new ViscosityTestData(); dpTestDate.SelectedDate = DateTime.Today; UpdateThermostatTime(); // --- 新增:初始化串口通信 --- InitSerialPort(); // 初始化 DispatcherTimer timer1.Interval = TimeSpan.FromMilliseconds(100); timer1.Tick += (s, e) => { txtTime1.Text = (stopwatch1.Elapsed.TotalSeconds).ToString("F3"); }; timer2.Interval = TimeSpan.FromMilliseconds(100); timer2.Tick += (s, e) => { txtTime2.Text = (stopwatch2.Elapsed.TotalSeconds).ToString("F3"); }; timer3.Interval = TimeSpan.FromMilliseconds(100); timer3.Tick += (s, e) => { txtTime3.Text = (stopwatch3.Elapsed.TotalSeconds).ToString("F3"); }; timer4.Interval = TimeSpan.FromMilliseconds(100); timer4.Tick += (s, e) => { txtTime4.Text = (stopwatch4.Elapsed.TotalSeconds).ToString("F3"); }; // -------------------------------- } private void InitSerialPort() { try { serialPort = new SerialPort("COM7", 9600, Parity.None, 8, StopBits.One); // 请根据实际COM口修改 serialPort.DataReceived += SerialPort_DataReceived; serialPort.Open(); // 启动定时器,每隔 5 秒读取一次 readTimer = new System.Timers.Timer(2000); readTimer.Elapsed += ReadTimer_Elapsed; readTimer.AutoReset = true; readTimer.Start(); txtStatus.Text = "串口已打开,正在读取数据..."; } catch (Exception ex) { MessageBox.Show($"串口打开失败: {ex.Message}\n请检查COM口号及连接。", "通信错误", MessageBoxButton.OK, MessageBoxImage.Warning); txtStatus.Text = "串口打开失败"; } } //private void ReadTimer_Elapsed(object sender, ElapsedEventArgs e) //{ // // 发送读保持寄存器指令:功能码 0x03,起始地址 0x0005,读取 2 个寄存器 // byte[] request = new byte[8]; // request[0] = slaveId; // request[1] = 0x03; // 功能码 // request[2] = 0x00; // 起始地址高字节 // request[3] = 0x05; // 起始地址低字节 (0x0005) // request[4] = 0x00; // 数量高字节 // request[5] = 0x02; // 数量低字节 (2 个寄存器) // byte[] crc = CalculateCRC16(request, 6); // request[6] = crc[0]; // request[7] = crc[1]; // lock (portLock) // { // if (serialPort != null && serialPort.IsOpen) // { // try // { // serialPort.Write(request, 0, request.Length); // } // catch (Exception ex) // { // Dispatcher.Invoke(() => txtStatus.Text = $"发送失败: {ex.Message}"); // } // } // } //} // 替换原有的 ReadTimer_Elapsed 方法 private void ReadTimer_Elapsed(object sender, ElapsedEventArgs e) { lock (portLock) { if (serialPort == null || !serialPort.IsOpen) return; // 从 0x0000 开始读取 20 个寄存器 byte[] request = new byte[8]; request[0] = slaveId; request[1] = 0x03; request[2] = 0x00; request[3] = 0x00; // 起始地址 0x0000 request[4] = 0x00; request[5] = 0x14; // 读取 20 个寄存器 (0x0000 ~ 0x0013) byte[] crc = CalculateCRC16(request, 6); request[6] = crc[0]; request[7] = crc[1]; try { serialPort.Write(request, 0, request.Length); } catch { } } } private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { System.Threading.Thread.Sleep(50); byte[] buffer = new byte[serialPort.BytesToRead]; serialPort.Read(buffer, 0, buffer.Length); if (buffer.Length < 9 || buffer[0] != slaveId || buffer[1] != 0x03) return; // 解析温度 (0x0005) int tempIndex = 3 + (0x0005 - 0x0000) * 2; ushort rawTemp = (ushort)((buffer[tempIndex] << 8) | buffer[tempIndex + 1]); double temp = rawTemp / 100.0; // 解析运行时间 (0x0007) - 这才是您要的恒温时间! int timeIndex = 3 + (0x0007 - 0x0000) * 2; ushort rawRunTime = (ushort)((buffer[timeIndex] << 8) | buffer[timeIndex + 1]); // 根据 0x000F 的值判断单位(0=分钟,1=小时) int unitIndex = 3 + (0x000F - 0x0000) * 2; ushort timeUnit = (ushort)((buffer[unitIndex] << 8) | buffer[unitIndex + 1]); int displayTime = rawRunTime; string unit = "分钟"; if (timeUnit == 1) { displayTime = rawRunTime; // 小时 unit = "小时"; } Dispatcher.Invoke(() => { txtTestTemp.Text = temp.ToString("F2"); // 只有当运行时间 >0 时才显示,否则显示等待状态 if (rawRunTime > 0) { txtThermostatTime.Text = displayTime.ToString(); txtStatus.Text = $"温度: {temp:F2}℃ 已运行: {displayTime} {unit} 时间单位寄存器: {timeUnit}"; } else { txtThermostatTime.Text = "0"; txtStatus.Text = $"温度: {temp:F2}℃ 等待启动... (运行时间=0)"; } }); } catch (Exception ex) { Dispatcher.Invoke(() => txtStatus.Text = $"接收错误: {ex.Message}"); } } //private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) //{ // try // { // System.Threading.Thread.Sleep(50); // 等待数据收完 // byte[] buffer = new byte[serialPort.BytesToRead]; // serialPort.Read(buffer, 0, buffer.Length); // // 最小有效响应长度:地址(1) + 功能码(1) + 字节数(1) + 数据(2*2) + CRC(2) = 9 // if (buffer.Length < 9 || buffer[0] != slaveId || buffer[1] != 0x03) // return; // // 提取数据:温度寄存器 (0x0005) 和 运行时间寄存器 (0x0007) // ushort rawTemp = (ushort)((buffer[3] << 8) | buffer[4]); // ushort rawRunTime = (ushort)((buffer[5] << 8) | buffer[6]); // double temp = rawTemp / 100.0; // 协议规定小数点2位 // int runTimeMinutes = rawRunTime; // 假设单位是分钟,直接显示 // // 更新 UI // Dispatcher.Invoke(() => // { // // 更新试验温度(如果希望保留手动输入能力,可添加判断条件,这里直接覆盖) // txtTestTemp.Text = temp.ToString("F2"); // // 更新恒温时间 // txtThermostatTime.Text = runTimeMinutes.ToString(); // txtStatus.Text = $"实时温度: {temp:F2}℃ 运行时间: {runTimeMinutes} min"; // }); // } // catch (Exception ex) // { // Dispatcher.Invoke(() => txtStatus.Text = $"接收错误: {ex.Message}"); // } //} private byte[] CalculateCRC16(byte[] data, int length) { ushort crc = 0xFFFF; for (int pos = 0; pos < length; pos++) { crc ^= data[pos]; for (int i = 8; i != 0; i--) { if ((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return new byte[] { (byte)(crc & 0xFF), (byte)(crc >> 8) }; } protected override void OnClosed(EventArgs e) { base.OnClosed(e); readTimer?.Stop(); readTimer?.Dispose(); if (serialPort != null && serialPort.IsOpen) { serialPort.Close(); serialPort.Dispose(); } } // 温度变化时自动更新恒温时间(表2) private void TxtTestTemp_TextChanged(object sender, TextChangedEventArgs e) { UpdateThermostatTime(); } private void UpdateThermostatTime() { if (double.TryParse(txtTestTemp.Text, out double temp)) { int time = Models.Constants.GetThermostatTime(temp); txtThermostatTime.Text = time.ToString(); } else { txtThermostatTime.Text = ""; } } // 加载示例数据 private void BtnLoadExample_Click(object sender, RoutedEventArgs e) { txtSampleID.Text = "S2026-001"; txtSampleName.Text = "润滑油"; txtTestTemp.Text = "50"; txtConstant.Text = "0.4780"; txtDensity.Text = "0.85"; txtOperator.Text = "张三"; txtRemark.Text = "示例数据"; txtTime1.Text = "322.4"; txtTime2.Text = "322.6"; txtTime3.Text = "321.0"; txtTime4.Text = ""; txtTime5.Text = ""; txtTime6.Text = ""; UpdateThermostatTime(); txtStatus.Text = "示例数据已加载"; } // 清空所有输入 private void BtnClearAll_Click(object sender, RoutedEventArgs e) { txtSampleID.Clear(); txtSampleName.Clear(); txtTestTemp.Clear(); txtConstant.Clear(); txtDensity.Clear(); txtOperator.Clear(); txtRemark.Clear(); txtTime1.Clear(); txtTime2.Clear(); txtTime3.Clear(); txtTime4.Clear(); txtTime5.Clear(); txtTime6.Clear(); txtAvgTime.Clear(); txtViscosity.Clear(); txtDynamicViscosity.Clear(); txtValidCount.Clear(); txtPrecisionJudge.Clear(); txtRepeatability.Clear(); txtReproducibility.Clear(); txtMeasuredRepeatability.Clear(); txtRepeatJudge.Clear(); lstRawTimes.ItemsSource = null; rawTimes.Clear(); // 停止并重置所有计时器 timer1.Stop(); timer2.Stop(); timer3.Stop(); timer4.Stop(); stopwatch1.Reset(); stopwatch2.Reset(); stopwatch3.Reset(); stopwatch4.Reset(); txtStatus.Text = "已清空"; } // 清空流动时间 private void BtnClearTimes_Click(object sender, RoutedEventArgs e) { txtTime1.Clear(); txtTime2.Clear(); txtTime3.Clear(); txtTime4.Clear(); txtTime5.Clear(); txtTime6.Clear(); rawTimes.Clear(); timer1.Stop(); timer2.Stop(); timer3.Stop(); timer4.Stop(); stopwatch1.Reset(); stopwatch2.Reset(); stopwatch3.Reset(); stopwatch4.Reset(); lstRawTimes.ItemsSource = null; } // 增加一次测量(在界面中添加更多输入框,为简化,我们已预留6个) private void BtnAddMeasurement_Click(object sender, RoutedEventArgs e) { // 本版本固定6个输入框,无需动态增加 MessageBox.Show("已支持最多6次测量,请直接在对应框中输入。", "提示"); } // 核心计算:运动粘度、动力粘度、精密度 private void BtnCalcViscosity_Click(object sender, RoutedEventArgs e) { try { // 1. 读取流动时间 rawTimes = GetTimeValues(); if (rawTimes.Count < 4) { MessageBox.Show("至少需要输入4次流动时间!", "数据不足", MessageBoxButton.OK, MessageBoxImage.Warning); return; } // 2. 读取常数、密度、温度 if (!double.TryParse(txtConstant.Text, out double constant)) { MessageBox.Show("请输入有效的粘度计常数 C", "参数错误", MessageBoxButton.OK, MessageBoxImage.Warning); return; } if (!double.TryParse(txtTestTemp.Text, out double temp)) { MessageBox.Show("请输入试验温度", "参数错误", MessageBoxButton.OK, MessageBoxImage.Warning); return; } double density = 0; if (!string.IsNullOrWhiteSpace(txtDensity.Text)) { if (!double.TryParse(txtDensity.Text, out density)) { MessageBox.Show("密度格式错误,将不计算动力粘度", "提示", MessageBoxButton.OK, MessageBoxImage.Information); } } // 3. 根据温度获取允许偏差(%) double allowedDeviationPercent = Models.Constants.GetAllowedDeviation(temp); double allowedDeviation = 0; // 4. 计算平均值、筛选有效数据 List validTimes = new List(); double avg = rawTimes.Average(); bool continueCalc = true; int maxIterations = 10; // 防止死循环 while (continueCalc && maxIterations-- > 0) { validTimes = rawTimes.Where(t => Math.Abs(t - avg) <= avg * allowedDeviationPercent / 100.0).ToList(); if (validTimes.Count >= 3) { double newAvg = validTimes.Average(); if (Math.Abs(newAvg - avg) < 0.0001) continueCalc = false; else avg = newAvg; } else { break; } } if (validTimes.Count < 3) { MessageBox.Show("有效测量次数不足3次,无法计算!请检查数据是否离散过大。", "精密度超差", MessageBoxButton.OK, MessageBoxImage.Error); return; } double finalAvg = validTimes.Average(); // 5. 计算运动粘度 double viscosity = constant * finalAvg; // 6. 计算动力粘度(如果有密度) double dynamicViscosity = 0; if (density > 0) { dynamicViscosity = viscosity * density; } // 7. 计算本次测量的重复性(相对极差) double measuredRepeatability = 0; if (validTimes.Count > 1) { measuredRepeatability = (validTimes.Max() - validTimes.Min()) / finalAvg * 100; } // 8. 获取标准重复性限 double stdRepeatability = Models.Constants.GetRepeatabilityLimit(temp); // 9. 更新UI txtAvgTime.Text = finalAvg.ToString("F3"); txtViscosity.Text = viscosity.ToString("F4"); // 四位有效数字 if (density > 0) txtDynamicViscosity.Text = dynamicViscosity.ToString("F4"); else txtDynamicViscosity.Text = "未计算"; txtValidCount.Text = $"{validTimes.Count} / {rawTimes.Count}"; txtMeasuredRepeatability.Text = measuredRepeatability.ToString("F2") + "%"; txtRepeatability.Text = stdRepeatability.ToString("F1") + "%"; txtReproducibility.Text = Models.Constants.GetReproducibilityLimit(temp).ToString("F1") + "%"; bool repeatOK = measuredRepeatability <= stdRepeatability; txtRepeatJudge.Text = repeatOK ? "合格" : $"超差 (允许 ≤ {stdRepeatability:F1}%)"; txtRepeatJudge.Foreground = repeatOK ? System.Windows.Media.Brushes.Green : System.Windows.Media.Brushes.Red; txtPrecisionJudge.Text = repeatOK ? "精密度符合要求" : "精密度不符合要求,建议重新测试"; // 10. 显示原始数据 lstRawTimes.ItemsSource = rawTimes.Select((t, i) => new { Display = $"第{i + 1}次: {t:F3}s" }).ToList(); // 11. 保存当前数据到模型 currentData.SampleID = txtSampleID.Text; currentData.SampleName = txtSampleName.Text; currentData.TestTemp = temp; currentData.Constant = constant; currentData.Density = density > 0 ? density : (double?)null; currentData.RawTimes = rawTimes; currentData.ValidTimes = validTimes; currentData.AvgTime = finalAvg; currentData.Viscosity = viscosity; currentData.DynamicViscosity = dynamicViscosity; currentData.Operator = txtOperator.Text; currentData.TestDate = dpTestDate.SelectedDate ?? DateTime.Now; currentData.Remark = txtRemark.Text; currentData.Repeatability = measuredRepeatability; currentData.IsRepeatabilityOK = repeatOK; txtStatus.Text = $"计算完成,有效次数 {validTimes.Count},运动粘度 {viscosity:G4} mm²/s"; } catch (Exception ex) { MessageBox.Show($"计算错误: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error); } } // 从文本框获取流动时间列表 private List GetTimeValues() { var list = new List(); AddIfValid(txtTime1, list); AddIfValid(txtTime2, list); AddIfValid(txtTime3, list); AddIfValid(txtTime4, list); AddIfValid(txtTime5, list); AddIfValid(txtTime6, list); return list; } private void AddIfValid(TextBox tb, List list) { if (!string.IsNullOrWhiteSpace(tb.Text) && double.TryParse(tb.Text, out double val)) { list.Add(val); } } // 生成原始记录 private void BtnGenRecord_Click(object sender, RoutedEventArgs e) { if (currentData.Viscosity <= 0) { MessageBox.Show("请先计算运动粘度!", "无数据", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var record = new TestRecordControl(); record.LoadData(currentData); ReportContent.Content = record; } // 生成测试报告 private void BtnGenReport_Click(object sender, RoutedEventArgs e) { if (currentData.Viscosity <= 0) { MessageBox.Show("请先计算运动粘度!", "无数据", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var report = new TestReportControl(); report.LoadData(currentData); ReportContent.Content = report; } // 打印预览 private void BtnPrintPreview_Click(object sender, RoutedEventArgs e) { if (ReportContent.Content == null) { MessageBox.Show("请先生成记录或报告", "无报表", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (ReportContent.Content is FrameworkElement visual) { PrintDialog printDialog = new PrintDialog(); if (printDialog.ShowDialog() == true) { printDialog.PrintVisual(visual, "粘度测试报表"); } } } // 第1次 private void BtnStart1_Click(object sender, RoutedEventArgs e) { timer1.Stop(); stopwatch1.Reset(); stopwatch1.Start(); timer1.Start(); } private void BtnStop1_Click(object sender, RoutedEventArgs e) { timer1.Stop(); stopwatch1.Stop(); txtTime1.Text = (stopwatch1.Elapsed.TotalSeconds).ToString("F3"); } // 第2次 private void BtnStart2_Click(object sender, RoutedEventArgs e) { timer2.Stop(); stopwatch2.Reset(); stopwatch2.Start(); timer2.Start(); } private void BtnStop2_Click(object sender, RoutedEventArgs e) { timer2.Stop(); stopwatch2.Stop(); txtTime2.Text = (stopwatch2.Elapsed.TotalSeconds).ToString("F3"); } // 第3次 private void BtnStart3_Click(object sender, RoutedEventArgs e) { timer3.Stop(); stopwatch3.Reset(); stopwatch3.Start(); timer3.Start(); } private void BtnStop3_Click(object sender, RoutedEventArgs e) { timer3.Stop(); stopwatch3.Stop(); txtTime3.Text = (stopwatch3.Elapsed.TotalSeconds).ToString("F3"); } // 第4次 private void BtnStart4_Click(object sender, RoutedEventArgs e) { timer4.Stop(); stopwatch4.Reset(); stopwatch4.Start(); timer4.Start(); } private void BtnStop4_Click(object sender, RoutedEventArgs e) { timer4.Stop(); stopwatch4.Stop(); txtTime4.Text = (stopwatch4.Elapsed.TotalSeconds).ToString("F3"); } } }