Files
PetroleumViscosityTest/MainWindow.xaml.cs
2026-02-24 18:24:09 +08:00

647 lines
24 KiB
C#
Raw 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 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<double> rawTimes = new List<double>();
// --- 新增 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<double> validTimes = new List<double>();
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<double> GetTimeValues()
{
var list = new List<double>();
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<double> 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");
}
}
}