添加项目文件。
This commit is contained in:
12
App.xaml
Normal file
12
App.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<Application x:Class="AciTester.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:AciTester.Converters"
|
||||
StartupUri="Views/MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<converters:BoolToColorConverter x:Key="BoolToColorConverter"/>
|
||||
<converters:BoolToStringConverter x:Key="BoolToStringConverter"/>
|
||||
<converters:InverseBoolConverter x:Key="InverseBoolConverter"/>
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
14
App.xaml.cs
Normal file
14
App.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace AciTester
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
10
AssemblyInfo.cs
Normal file
10
AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
BIN
CSI-Z173 吸入制剂药物测试仪(1).gxw
Normal file
BIN
CSI-Z173 吸入制剂药物测试仪(1).gxw
Normal file
Binary file not shown.
22
Converters/BoolToColorConverter.cs
Normal file
22
Converters/BoolToColorConverter.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AciTester.Converters
|
||||
{
|
||||
public class BoolToColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool b)
|
||||
return b ? Brushes.Green : Brushes.Red;
|
||||
return Brushes.Gray;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Converters/BoolToStringConverter.cs
Normal file
21
Converters/BoolToStringConverter.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace AciTester.Converters
|
||||
{
|
||||
public class BoolToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool b)
|
||||
return b ? "运行" : "停止";
|
||||
return "未知";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Converters/BoolToVisibilityConverter.cs
Normal file
33
Converters/BoolToVisibilityConverter.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace AciTester.Converters
|
||||
{
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return (value is bool b && b) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return !(value is bool b && b);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Models/CalibrationConfig.cs
Normal file
25
Models/CalibrationConfig.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace AciTester.Models
|
||||
{
|
||||
public partial class CalibrationConfig : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private float flowCalibration = 1.0f; // D1328
|
||||
|
||||
[ObservableProperty]
|
||||
private float temperatureCalibration = 1.0f; // D1378
|
||||
|
||||
[ObservableProperty]
|
||||
private float pumpPressureCalibration = 1.0f; // D1428
|
||||
|
||||
[ObservableProperty]
|
||||
private float impactorPressureCalibration = 1.0f; // D1478
|
||||
|
||||
[ObservableProperty]
|
||||
private float flowLowLimit = 25.0f; // D1332 低限
|
||||
|
||||
[ObservableProperty]
|
||||
private float flowHighLimit = 32.0f; // D1332 高限
|
||||
}
|
||||
}
|
||||
101
Models/PlcConfiguration.cs
Normal file
101
Models/PlcConfiguration.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AciTester.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 配置类,用于存储从 appsettings.json 读取的 Modbus 参数和寄存器地址。
|
||||
/// </summary>
|
||||
public class PlcConfiguration
|
||||
{
|
||||
// ========== 网络连接参数 ==========
|
||||
public string IpAddress { get; set; } // PLC IP 地址
|
||||
public int Port { get; set; } // Modbus TCP 端口
|
||||
public byte SlaveId { get; set; } = 1; // 从站地址(默认1)
|
||||
|
||||
// 以下属性用于与上位机交互(但实际按工位读取,此处保留兼容)
|
||||
public ushort PressureRegister { get; set; } // 不再使用,保留兼容
|
||||
public ushort WetFlowRegister { get; set; } // 湿膜流量寄存器起始地址
|
||||
|
||||
|
||||
public ushort WetFlowRegister2 { get; set; } // 湿膜流量寄存器起始地址
|
||||
|
||||
|
||||
public ushort WetFlowRegister3 { get; set; } // 湿膜流量寄存器起始地址
|
||||
|
||||
|
||||
// ========== 工位专用寄存器 ==========
|
||||
public ushort PressureRegisterStation1 { get; set; } // 工位1 压力寄存器起始地址
|
||||
public ushort PressureRegisterStation2 { get; set; } // 工位2
|
||||
public ushort PressureRegisterStation3 { get; set; } // 工位3
|
||||
|
||||
|
||||
|
||||
|
||||
public ushort PumpCoil { get; set; } // 高压超限 M180
|
||||
|
||||
|
||||
public ushort FlowRegister { get; set; } // 高压超限 M180
|
||||
|
||||
|
||||
|
||||
|
||||
// 新增地址
|
||||
public ushort FlowCalibrationReg { get; set; } = 1328;
|
||||
public ushort FlowProtectReg { get; set; } = 1332;
|
||||
public ushort TemperatureReg { get; set; } = 1380;
|
||||
public ushort TempCalibrationReg { get; set; } = 1378;
|
||||
public ushort PumpPressureReg { get; set; } = 1430;
|
||||
public ushort PumpPressureCalibReg { get; set; } = 1428;
|
||||
public ushort ImpactorPressureReg { get; set; } = 1480;
|
||||
public ushort ImpactorPressureCalibReg { get; set; } = 1478;
|
||||
public ushort ImpactorPressureCalibCoil { get; set; } = 1303; // M1303
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLC 服务接口,定义与 Modbus 设备通信的方法。
|
||||
/// </summary>
|
||||
public interface IPlcService
|
||||
{
|
||||
/// <summary> 读取指定工位的压力(浮点数) </summary>
|
||||
/// <param name="stationId">工位号 1~3</param>
|
||||
Task<float> ReadPressureAsync(int stationId);
|
||||
|
||||
/// <summary> 读取湿膜流量(浮点数) </summary>
|
||||
Task<float> ReadWetFlowAsync(int stationId);
|
||||
|
||||
|
||||
/// <summary> 写入线圈(如 M 元件) </summary>
|
||||
Task WriteCoilAsync(ushort coilAddress, bool value);
|
||||
|
||||
/// <summary> 写入单个寄存器(16位) </summary>
|
||||
Task WriteRegisterAsync(ushort registerAddress, ushort value);
|
||||
|
||||
/// <summary> 读取线圈状态(如 M 元件的 ON/OFF) </summary>
|
||||
Task<bool> ReadCoilAsync(ushort coilAddress);
|
||||
|
||||
/// <summary> 读取连续多个保持寄存器(16位) </summary>
|
||||
Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count);
|
||||
|
||||
/// <summary> 写入单个保持寄存器(16位) </summary>
|
||||
Task WriteSingleRegisterAsync(ushort registerAddress, ushort value);
|
||||
|
||||
|
||||
Task WriteMultipleRegistersAsync(ushort registerAddress, float value);
|
||||
|
||||
float UshortToFloat(ushort P1, ushort P2);
|
||||
|
||||
Task<float> ReadFloatAsync(ushort startAddress);
|
||||
|
||||
Task EnsureConnectedAsync(int retryCount = 3);
|
||||
|
||||
void Dispose();
|
||||
|
||||
bool IsConnected { get; } // 新增
|
||||
}
|
||||
}
|
||||
25
Models/RealTimeData.cs
Normal file
25
Models/RealTimeData.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace AciTester.Models
|
||||
{
|
||||
public partial class RealTimeData : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private float rawFlow; // D1330原始值
|
||||
|
||||
[ObservableProperty]
|
||||
private float calibratedFlow; // 校准后流量
|
||||
|
||||
[ObservableProperty]
|
||||
private float temperature; // D1380
|
||||
|
||||
[ObservableProperty]
|
||||
private float pumpPressure; // D1430
|
||||
|
||||
[ObservableProperty]
|
||||
private float impactorPressure; // D1480
|
||||
|
||||
[ObservableProperty]
|
||||
private float differentialPressure; // 压差 = impactorPressure - pumpPressure
|
||||
}
|
||||
}
|
||||
20
Models/StageData.cs
Normal file
20
Models/StageData.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace AciTester.Models;
|
||||
|
||||
public partial class StageData : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string stageName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private double cutoffDiameter; // 截止直径 (μm)
|
||||
|
||||
[ObservableProperty]
|
||||
private double initialWeight; // 测前质量 (g)
|
||||
|
||||
[ObservableProperty]
|
||||
private double finalWeight; // 测后质量 (g)
|
||||
|
||||
public double NetWeight => finalWeight - initialWeight;
|
||||
}
|
||||
14
Models/TestResult.cs
Normal file
14
Models/TestResult.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace AciTester.Models;
|
||||
|
||||
public class TestResult
|
||||
{
|
||||
public DateTime TestTime { get; set; }
|
||||
public double TotalMass { get; set; } // 总质量 F (g)
|
||||
public double FineParticleDose { get; set; } // 微细粒子剂量 FPD (mg)
|
||||
public double FineParticleFraction { get; set; } // 微细粒子分数 FPF (%)
|
||||
public List<StageData> Stages { get; set; }
|
||||
public double FlowRate { get; set; } // 实际采样流量 (L/min)
|
||||
|
||||
public float Temperature { get; set; }
|
||||
public float DifferentialPressure { get; set; }
|
||||
}
|
||||
64
Services/ExcelReportService.cs
Normal file
64
Services/ExcelReportService.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using AciTester.Models;
|
||||
using OfficeOpenXml;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AciTester.Services
|
||||
{
|
||||
public class ExcelReportService : IReportService
|
||||
{
|
||||
public async Task GenerateReportAsync(TestResult result, string filePath)
|
||||
{
|
||||
ExcelPackage.LicenseContext = OfficeOpenXml.LicenseContext.NonCommercial;
|
||||
using var package = new ExcelPackage();
|
||||
var sheet = package.Workbook.Worksheets.Add("ACI测试报告");
|
||||
|
||||
// 标题
|
||||
sheet.Cells["A1"].Value = "中国药典2025版四部通则0951装置3测试报告";
|
||||
sheet.Cells["A1:C1"].Merge = true;
|
||||
sheet.Cells["A1"].Style.Font.Bold = true;
|
||||
sheet.Cells["A1"].Style.Font.Size = 16;
|
||||
|
||||
// 基本信息
|
||||
sheet.Cells["A3"].Value = "测试时间:";
|
||||
sheet.Cells["B3"].Value = result.TestTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
sheet.Cells["A4"].Value = "采样流量(L/min):";
|
||||
sheet.Cells["B4"].Value = result.FlowRate;
|
||||
sheet.Cells["A5"].Value = "总质量F(g):";
|
||||
sheet.Cells["B5"].Value = result.TotalMass;
|
||||
|
||||
// 在报告生成时添加
|
||||
sheet.Cells["A6"].Value = "测试环境:";
|
||||
sheet.Cells["B6"].Value = $"流量: {result.FlowRate:F2} L/min, 温度: {result.Temperature:F1}℃, 压差: {result.DifferentialPressure:F2} kPa";
|
||||
|
||||
// 微细粒子指标
|
||||
sheet.Cells["A7"].Value = "微细粒子剂量(FPD)";
|
||||
sheet.Cells["B7"].Value = $"{result.FineParticleDose:F2} mg";
|
||||
sheet.Cells["A8"].Value = "微细粒子分数(FPF)";
|
||||
sheet.Cells["B8"].Value = $"{result.FineParticleFraction:F2}%";
|
||||
|
||||
// 各级数据表
|
||||
sheet.Cells["A10"].Value = "层级";
|
||||
sheet.Cells["B10"].Value = "截止直径(μm)";
|
||||
sheet.Cells["C10"].Value = "净重(g)";
|
||||
sheet.Cells["D10"].Value = "占比(%)";
|
||||
sheet.Cells["A10:D10"].Style.Font.Bold = true;
|
||||
|
||||
int row = 11;
|
||||
double total = result.TotalMass;
|
||||
foreach (var stage in result.Stages)
|
||||
{
|
||||
double percent = total > 0 ? stage.NetWeight / total * 100 : 0;
|
||||
sheet.Cells[row, 1].Value = stage.StageName;
|
||||
sheet.Cells[row, 2].Value = stage.CutoffDiameter > 0 ? stage.CutoffDiameter.ToString("F1") : "—";
|
||||
sheet.Cells[row, 3].Value = stage.NetWeight;
|
||||
sheet.Cells[row, 4].Value = percent.ToString("F2");
|
||||
row++;
|
||||
}
|
||||
|
||||
sheet.Cells.AutoFitColumns();
|
||||
await package.SaveAsAsync(new FileInfo(filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Services/IReportService.cs
Normal file
10
Services/IReportService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using AciTester.Models;
|
||||
|
||||
namespace AciTester.Services
|
||||
{
|
||||
public interface IReportService
|
||||
{
|
||||
Task GenerateReportAsync(TestResult result, string filePath);
|
||||
}
|
||||
}
|
||||
207
Services/ModbusTcpPlcService.cs
Normal file
207
Services/ModbusTcpPlcService.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using Modbus.Device;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AciTester.Models;
|
||||
|
||||
namespace AciTester.Services
|
||||
{
|
||||
public class ModbusTcpPlcService : IPlcService, IDisposable
|
||||
{
|
||||
private readonly PlcConfiguration _config;
|
||||
private TcpClient _tcpClient;
|
||||
private IModbusMaster _master;
|
||||
|
||||
public ModbusTcpPlcService(PlcConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task EnsureConnectedAsync(int retryCount = 3)
|
||||
{
|
||||
if (_tcpClient != null && _tcpClient.Connected)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < retryCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
_tcpClient?.Close();
|
||||
_tcpClient = new TcpClient();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
// 现在可以直接使用扩展方法
|
||||
await _tcpClient.ConnectAsync(_config.IpAddress, _config.Port).WithCancellation(cts.Token);
|
||||
_master = ModbusIpMaster.CreateIp(_tcpClient);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (i < retryCount - 1)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"连接失败,{500}ms 后重试... {ex.Message}");
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
throw new Exception($"无法连接到 PLC ({_config.IpAddress}:{_config.Port}),请检查网络和 PLC 状态。");
|
||||
}
|
||||
|
||||
// 读取两个连续的保持寄存器,转换为32位浮点数(假设大端模式)
|
||||
public async Task<float> ReadFloatAsync(ushort startAddress)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
var registers = await ReadHoldingRegistersAsync(startAddress, 2);
|
||||
return UshortToFloat(registers[1], registers[0]);
|
||||
}
|
||||
|
||||
|
||||
public async Task<float> ReadPressureAsync() =>
|
||||
await ReadFloatAsync(_config.PressureRegister);
|
||||
|
||||
public async Task<float> ReadWetFlowAsync(int stationId)
|
||||
{
|
||||
ushort startAddress = stationId switch
|
||||
{
|
||||
1 => _config.WetFlowRegister,
|
||||
2 => _config.WetFlowRegister2,
|
||||
3 => _config.WetFlowRegister3,
|
||||
};
|
||||
return await ReadFloatAsync(startAddress);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task WriteCoilAsync(ushort coilAddress, bool value)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
await _master.WriteSingleCoilAsync(_config.SlaveId, coilAddress, value);
|
||||
}
|
||||
|
||||
public async Task WriteRegisterAsync(ushort registerAddress, ushort value)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
await Task.Delay(100);
|
||||
await _master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> ReadCoilAsync(ushort coilAddress)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
await Task.Delay(100);
|
||||
bool[] result = await _master?.ReadCoilsAsync(_config.SlaveId, coilAddress, 1);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
public bool IsConnected => _tcpClient != null && _tcpClient.Connected;
|
||||
|
||||
public async Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
// await Task.Delay(100);
|
||||
return await _master.ReadHoldingRegistersAsync(_config.SlaveId, startAddress, count);
|
||||
}
|
||||
|
||||
public async Task WriteSingleRegisterAsync(ushort registerAddress, ushort value)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
int val = (int)value;
|
||||
await Task.Delay(100);
|
||||
await _master.WriteMultipleRegistersAsync(1, registerAddress, intToushorts(val));
|
||||
}
|
||||
|
||||
public async Task WriteMultipleRegistersAsync(ushort registerAddress, float value)
|
||||
{
|
||||
await EnsureConnectedAsync();
|
||||
await Task.Delay(100);
|
||||
await _master.WriteMultipleRegistersAsync(_config.SlaveId, registerAddress, SplitFloatToUShortArray((float)value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Int转为ushort数组发送
|
||||
/// </summary>
|
||||
/// <param name="res"></param>
|
||||
/// <returns>返回ushort数组</returns>
|
||||
private ushort[] intToushorts(int res)
|
||||
{
|
||||
ushort ust1 = (ushort)(res >> 16);
|
||||
ushort ust2 = (ushort)res;
|
||||
return new ushort[] { ust2, ust1 };
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Float转为Ushort数组发送
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns>返回ushort数组</returns>
|
||||
public ushort[] SplitFloatToUShortArray(float value)
|
||||
{
|
||||
byte[] floatBytes = BitConverter.GetBytes(value);
|
||||
ushort[] ushortArray = new ushort[floatBytes.Length / 2];
|
||||
|
||||
for (int i = 0, j = 0; i < floatBytes.Length; i += 2, j++)
|
||||
{
|
||||
ushortArray[j] = BitConverter.ToUInt16(floatBytes, i);
|
||||
}
|
||||
|
||||
return ushortArray;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ushort转为float类型
|
||||
/// </summary>
|
||||
/// <param name="P1"></param>
|
||||
/// <param name="P2"></param>
|
||||
/// <returns>float型数据</returns>
|
||||
public float UshortToFloat(ushort P1, ushort P2)
|
||||
{
|
||||
int intSign, intSignRest, intExponent, intExponentRest;
|
||||
float faResult, faDigit;
|
||||
intSign = P1 / 32768;
|
||||
intSignRest = P1 % 32768;
|
||||
intExponent = intSignRest / 128;
|
||||
intExponentRest = intSignRest % 128;
|
||||
faDigit = (float)(intExponentRest * 65536 + P2) / 8388608;
|
||||
faResult = (float)Math.Pow(-1, intSign) * (float)Math.Pow(2, intExponent - 127) * (faDigit + 1);
|
||||
return faResult;
|
||||
}
|
||||
|
||||
// 新增读取压力(根据工位)
|
||||
public async Task<float> ReadPressureAsync(int stationId)
|
||||
{
|
||||
ushort startAddress = stationId switch
|
||||
{
|
||||
1 => _config.PressureRegisterStation1,
|
||||
2 => _config.PressureRegisterStation2,
|
||||
3 => _config.PressureRegisterStation3,
|
||||
_ => throw new ArgumentException("Invalid station")
|
||||
};
|
||||
return await ReadFloatAsync(startAddress);
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_master?.Dispose();
|
||||
_tcpClient?.Close();
|
||||
_tcpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class TaskExtensions
|
||||
{
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
|
||||
{
|
||||
if (task != await Task.WhenAny(task, tcs.Task))
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
await task;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
ViewModels/ConfigViewModel.cs
Normal file
79
ViewModels/ConfigViewModel.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using AciTester.Models;
|
||||
using AciTester.Services;
|
||||
using System.Windows;
|
||||
|
||||
namespace AciTester.ViewModels
|
||||
{
|
||||
public partial class ConfigViewModel : ObservableObject
|
||||
{
|
||||
private readonly IPlcService _plcService;
|
||||
private readonly PlcConfiguration _config;
|
||||
private readonly CalibrationConfig _calib;
|
||||
|
||||
public ConfigViewModel(IPlcService plcService, PlcConfiguration config, CalibrationConfig calib)
|
||||
{
|
||||
_plcService = plcService;
|
||||
_config = config;
|
||||
_calib = calib;
|
||||
LoadConfigCommand = new AsyncRelayCommand(LoadConfigAsync);
|
||||
SaveConfigCommand = new AsyncRelayCommand(SaveConfigAsync);
|
||||
}
|
||||
|
||||
public IAsyncRelayCommand LoadConfigCommand { get; }
|
||||
public IAsyncRelayCommand SaveConfigCommand { get; }
|
||||
|
||||
public CalibrationConfig Calibration => _calib;
|
||||
|
||||
private async Task LoadConfigAsync()
|
||||
{
|
||||
if (!_plcService.IsConnected)
|
||||
{
|
||||
MessageBox.Show("请先连接PLC", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_calib.FlowCalibration = await _plcService.ReadFloatAsync(_config.FlowCalibrationReg);
|
||||
_calib.TemperatureCalibration = await _plcService.ReadFloatAsync(_config.TempCalibrationReg);
|
||||
_calib.PumpPressureCalibration = await _plcService.ReadFloatAsync(_config.PumpPressureCalibReg);
|
||||
_calib.ImpactorPressureCalibration = await _plcService.ReadFloatAsync(_config.ImpactorPressureCalibReg);
|
||||
// 读取流量保护值(假设高限低限分别存储,这里简化)
|
||||
var protect = await _plcService.ReadFloatAsync(_config.FlowProtectReg);
|
||||
_calib.FlowLowLimit = protect - 5; // 示例,实际根据协议解析
|
||||
_calib.FlowHighLimit = protect + 5;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
MessageBox.Show($"读取配置失败: {ex.Message}", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveConfigAsync()
|
||||
{
|
||||
if (!_plcService.IsConnected)
|
||||
{
|
||||
MessageBox.Show("请先连接PLC", "提示");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await _plcService.WriteMultipleRegistersAsync(_config.FlowCalibrationReg, _calib.FlowCalibration);
|
||||
await _plcService.WriteMultipleRegistersAsync(_config.TempCalibrationReg, _calib.TemperatureCalibration);
|
||||
await _plcService.WriteMultipleRegistersAsync(_config.PumpPressureCalibReg, _calib.PumpPressureCalibration);
|
||||
await _plcService.WriteMultipleRegistersAsync(_config.ImpactorPressureCalibReg, _calib.ImpactorPressureCalibration);
|
||||
// 写入流量保护值(需根据实际协议拆分高低限)
|
||||
MessageBox.Show("配置保存成功", "提示");
|
||||
// 可选:触发重新校准
|
||||
await _plcService.WriteCoilAsync(_config.ImpactorPressureCalibCoil, true);
|
||||
await Task.Delay(100);
|
||||
await _plcService.WriteCoilAsync(_config.ImpactorPressureCalibCoil, false);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
MessageBox.Show($"保存失败: {ex.Message}", "错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
305
ViewModels/MainViewModel.cs
Normal file
305
ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using AciTester.Models;
|
||||
using AciTester.Services;
|
||||
using AciTester.Views;
|
||||
|
||||
namespace AciTester.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
public readonly IPlcService _plcService;
|
||||
private readonly IReportService _reportService;
|
||||
public readonly PlcConfiguration _config;
|
||||
private CancellationTokenSource _testCts;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isConnected;
|
||||
|
||||
[ObservableProperty]
|
||||
private string connectionStatus = "未连接";
|
||||
|
||||
[ObservableProperty]
|
||||
private float currentFlow;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isPumpRunning;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isTesting;
|
||||
|
||||
[ObservableProperty]
|
||||
private int sampleTimeSeconds = 60; // 默认采样60秒
|
||||
|
||||
[ObservableProperty]
|
||||
private int remainingSeconds;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<StageData> stages;
|
||||
|
||||
[ObservableProperty]
|
||||
private TestResult currentResult;
|
||||
|
||||
// 在 MainViewModel 中添加
|
||||
[ObservableProperty]
|
||||
private RealTimeData realTime;
|
||||
|
||||
[ObservableProperty]
|
||||
private CalibrationConfig calibration;
|
||||
|
||||
private Timer _dataTimer;
|
||||
public MainViewModel()
|
||||
{
|
||||
_config = new PlcConfiguration();
|
||||
_plcService = new ModbusTcpPlcService(_config);
|
||||
_reportService = new ExcelReportService();
|
||||
|
||||
// 初始化ACI 8级 + 滤膜
|
||||
Stages = new ObservableCollection<StageData>
|
||||
{
|
||||
new StageData { StageName = "Stage 0", CutoffDiameter = 9.0 },
|
||||
new StageData { StageName = "Stage 1", CutoffDiameter = 5.8 },
|
||||
new StageData { StageName = "Stage 2", CutoffDiameter = 4.7 },
|
||||
new StageData { StageName = "Stage 3", CutoffDiameter = 3.3 },
|
||||
new StageData { StageName = "Stage 4", CutoffDiameter = 2.1 },
|
||||
new StageData { StageName = "Stage 5", CutoffDiameter = 1.1 },
|
||||
new StageData { StageName = "Stage 6", CutoffDiameter = 0.7 },
|
||||
new StageData { StageName = "Stage 7", CutoffDiameter = 0.4 },
|
||||
new StageData { StageName = "Filter", CutoffDiameter = 0.0 }
|
||||
};
|
||||
|
||||
ConnectCommand = new AsyncRelayCommand(ConnectAsync);
|
||||
DisconnectCommand = new RelayCommand(Disconnect);
|
||||
StartTestCommand = new AsyncRelayCommand(StartTestAsync);
|
||||
CalculateCommand = new RelayCommand(CalculateResult);
|
||||
ExportReportCommand = new AsyncRelayCommand(ExportReportAsync);
|
||||
|
||||
// 在构造函数中初始化
|
||||
RealTime = new RealTimeData();
|
||||
Calibration = new CalibrationConfig();
|
||||
|
||||
|
||||
//this.Loaded += (s, e) =>
|
||||
//{
|
||||
// var keyGesture = new KeyGesture(Key.P, ModifierKeys.Control);
|
||||
// var inputBinding = new InputBinding(new RelayCommand(OpenConfigWindow), keyGesture);
|
||||
// this.InputBindings.Add(inputBinding);
|
||||
//};
|
||||
}
|
||||
|
||||
public IAsyncRelayCommand ConnectCommand { get; }
|
||||
public IRelayCommand DisconnectCommand { get; }
|
||||
public IAsyncRelayCommand StartTestCommand { get; }
|
||||
public IRelayCommand CalculateCommand { get; }
|
||||
public IAsyncRelayCommand ExportReportCommand { get; }
|
||||
|
||||
private async Task ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _plcService.EnsureConnectedAsync();
|
||||
IsConnected = true;
|
||||
ConnectionStatus = "已连接";
|
||||
_ = Task.Run(ReadFlowLoop);
|
||||
_ = Task.Run(ReadRealTimeLoop); // 新增
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"连接失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
IsConnected = false;
|
||||
ConnectionStatus = "连接失败";
|
||||
}
|
||||
}
|
||||
|
||||
private void Disconnect()
|
||||
{
|
||||
_plcService.Dispose();
|
||||
IsConnected = false;
|
||||
ConnectionStatus = "未连接";
|
||||
}
|
||||
|
||||
private async Task ReadFlowLoop()
|
||||
{
|
||||
while (IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
var flow = await _plcService.ReadFloatAsync(_config.FlowRegister);
|
||||
Application.Current.Dispatcher.Invoke(() => CurrentFlow = flow);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
catch { break; }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartTestAsync()
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
await ConnectAsync();
|
||||
if (!IsConnected) return;
|
||||
}
|
||||
|
||||
if (IsTesting) return;
|
||||
|
||||
IsTesting = true;
|
||||
_testCts = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
// 启动真空泵
|
||||
await _plcService.WriteCoilAsync(_config.PumpCoil, true);
|
||||
IsPumpRunning = true;
|
||||
|
||||
// 倒计时
|
||||
RemainingSeconds = SampleTimeSeconds;
|
||||
for (int i = 0; i < SampleTimeSeconds; i++)
|
||||
{
|
||||
if (_testCts.Token.IsCancellationRequested) break;
|
||||
await Task.Delay(1000);
|
||||
RemainingSeconds--;
|
||||
}
|
||||
|
||||
// 停止泵
|
||||
await _plcService.WriteCoilAsync(_config.PumpCoil, false);
|
||||
IsPumpRunning = false;
|
||||
|
||||
MessageBox.Show($"采样完成!\n实际采样时间: {SampleTimeSeconds} 秒\n请进行称重并录入数据。",
|
||||
"提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"测试异常: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
// 尝试停止泵
|
||||
try { await _plcService.WriteCoilAsync(_config.PumpCoil, false); } catch { }
|
||||
IsPumpRunning = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsTesting = false;
|
||||
_testCts?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateResult()
|
||||
{
|
||||
double totalMass = 0;
|
||||
foreach (var stage in Stages)
|
||||
totalMass += stage.NetWeight;
|
||||
|
||||
if (totalMass <= 0)
|
||||
{
|
||||
MessageBox.Show("总质量为零,无法计算", "警告", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 微细粒子剂量:截止直径 ≤ 5μm 的级 + 滤膜
|
||||
double fineMass = 0;
|
||||
foreach (var stage in Stages)
|
||||
{
|
||||
if (stage.CutoffDiameter <= 5.0 && stage.CutoffDiameter > 0)
|
||||
fineMass += stage.NetWeight;
|
||||
}
|
||||
fineMass += Stages[8].NetWeight; // 滤膜
|
||||
|
||||
double fpd = fineMass * 1000; // mg
|
||||
double fpf = (fineMass / totalMass) * 100;
|
||||
|
||||
CurrentResult = new TestResult
|
||||
{
|
||||
TestTime = DateTime.Now,
|
||||
TotalMass = totalMass,
|
||||
FineParticleDose = fpd,
|
||||
FineParticleFraction = fpf,
|
||||
Stages = Stages.ToList(),
|
||||
FlowRate = CurrentFlow,
|
||||
Temperature = RealTime.Temperature, // 新增
|
||||
DifferentialPressure = RealTime.DifferentialPressure // 新增
|
||||
};
|
||||
|
||||
MessageBox.Show($"计算完成\n总质量: {totalMass:F4} g\n微细粒子剂量: {fpd:F2} mg\n微细粒子分数: {fpf:F2}%",
|
||||
"计算结果", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async Task ExportReportAsync()
|
||||
{
|
||||
if (CurrentResult == null)
|
||||
{
|
||||
MessageBox.Show("请先计算测试结果", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var saveDialog = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Filter = "Excel文件|*.xlsx",
|
||||
FileName = $"ACI_Test_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"
|
||||
};
|
||||
if (saveDialog.ShowDialog() == true)
|
||||
{
|
||||
await _reportService.GenerateReportAsync(CurrentResult, saveDialog.FileName);
|
||||
MessageBox.Show("报告已保存", "完成", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 启动定时读取(连接成功后)
|
||||
private async Task ReadRealTimeLoop()
|
||||
{
|
||||
while (IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取原始流量
|
||||
var rawFlow = await _plcService.ReadFloatAsync(_config.FlowRegister);
|
||||
// 校准流量 = 原始流量 * 流量系数
|
||||
var calibrated = rawFlow * Calibration.FlowCalibration;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
RealTime.RawFlow = rawFlow;
|
||||
RealTime.CalibratedFlow = calibrated;
|
||||
});
|
||||
|
||||
// 温度
|
||||
var temp = await _plcService.ReadFloatAsync(_config.TemperatureReg);
|
||||
var calibratedTemp = temp * Calibration.TemperatureCalibration;
|
||||
Application.Current.Dispatcher.Invoke(() => RealTime.Temperature = calibratedTemp);
|
||||
|
||||
// 泵端压力
|
||||
var pumpPres = await _plcService.ReadFloatAsync(_config.PumpPressureReg);
|
||||
var calibratedPump = pumpPres * Calibration.PumpPressureCalibration;
|
||||
Application.Current.Dispatcher.Invoke(() => RealTime.PumpPressure = calibratedPump);
|
||||
|
||||
// 撞击器端压力
|
||||
var impPres = await _plcService.ReadFloatAsync(_config.ImpactorPressureReg);
|
||||
var calibratedImp = impPres * Calibration.ImpactorPressureCalibration;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
RealTime.ImpactorPressure = calibratedImp;
|
||||
RealTime.DifferentialPressure = calibratedImp - calibratedPump;
|
||||
});
|
||||
|
||||
// 流量报警检查
|
||||
if (calibrated < Calibration.FlowLowLimit || calibrated > Calibration.FlowHighLimit)
|
||||
{
|
||||
// 可触发界面警告
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
MessageBox.Show($"流量异常: {calibrated:F2} L/min", "警告", MessageBoxButton.OK, MessageBoxImage.Warning));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
50
Views/ConfigWindow.xaml
Normal file
50
Views/ConfigWindow.xaml
Normal file
@@ -0,0 +1,50 @@
|
||||
<Window x:Class="AciTester.Views.ConfigWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="参数校准配置 - Ctrl+P" Height="450" Width="500"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<GroupBox Header="流量校准" Grid.Row="0">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="流量系数:" Width="80" VerticalAlignment="Center"/>
|
||||
<TextBox Text="{Binding Calibration.FlowCalibration}" Width="100"/>
|
||||
<TextBlock Text=" 保护低限:" Margin="20,0,0,0"/>
|
||||
<TextBox Text="{Binding Calibration.FlowLowLimit}" Width="80"/>
|
||||
<TextBlock Text="高限:" Margin="5,0,0,0"/>
|
||||
<TextBox Text="{Binding Calibration.FlowHighLimit}" Width="80"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="温度校准" Grid.Row="1" Margin="0,10">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="温度系数:" Width="80"/>
|
||||
<TextBox Text="{Binding Calibration.TemperatureCalibration}" Width="100"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="压力校准(泵端)" Grid.Row="2">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="泵端压力系数:" Width="120"/>
|
||||
<TextBox Text="{Binding Calibration.PumpPressureCalibration}" Width="100"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="压力校准(撞击器端)" Grid.Row="3" Margin="0,10">
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="撞击器端压力系数:" Width="140"/>
|
||||
<TextBox Text="{Binding Calibration.ImpactorPressureCalibration}" Width="100"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10">
|
||||
<Button Command="{Binding LoadConfigCommand}" Content="读取" Width="80" Margin="5"/>
|
||||
<Button Command="{Binding SaveConfigCommand}" Content="保存" Width="80" Margin="5"/>
|
||||
<Button Click="CloseWindow" Content="关闭" Width="80" Margin="5"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
32
Views/ConfigWindow.xaml.cs
Normal file
32
Views/ConfigWindow.xaml.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace AciTester.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// ConfigWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ConfigWindow : Window
|
||||
{
|
||||
public ConfigWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CloseWindow(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
144
Views/MainWindow.xaml
Normal file
144
Views/MainWindow.xaml
Normal file
@@ -0,0 +1,144 @@
|
||||
<Window x:Class="AciTester.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:AciTester.ViewModels"
|
||||
Title="ACI测试系统 - 中国药典2025装置3"
|
||||
Height="768" Width="1024"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Window.DataContext>
|
||||
<local:MainViewModel/>
|
||||
</Window.DataContext>
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 状态栏 (Row 0) -->
|
||||
<StatusBar Grid.Row="0">
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="状态:"/>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{Binding ConnectionStatus}" Foreground="{Binding IsConnected, Converter={StaticResource BoolToColorConverter}}"/>
|
||||
</StatusBarItem>
|
||||
<Separator/>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="流量:"/>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{Binding CurrentFlow, StringFormat='{}{0:F2} L/min'}"/>
|
||||
</StatusBarItem>
|
||||
<Separator/>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="泵状态:"/>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{Binding IsPumpRunning, Converter={StaticResource BoolToStringConverter}}"/>
|
||||
</StatusBarItem>
|
||||
<Separator/>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="倒计时:" Visibility="{Binding IsTesting, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{Binding RemainingSeconds, StringFormat='{}{0} s'}" Visibility="{Binding IsTesting, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
|
||||
<!-- 实时监测参数 (Row 1) -->
|
||||
<GroupBox Header="实时监测参数" Grid.Row="1" Margin="0,5">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Vertical">
|
||||
<TextBlock Text="流量 (L/min)" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding RealTime.CalibratedFlow, StringFormat='{}{0:F2}'}" FontSize="18" Foreground="Blue"/>
|
||||
<TextBlock Text="(目标: 28.3)" FontSize="10" Foreground="Gray"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Vertical">
|
||||
<TextBlock Text="温度 (℃)" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding RealTime.Temperature, StringFormat='{}{0:F1}'}" FontSize="18"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Vertical">
|
||||
<TextBlock Text="泵端压力 (kPa)" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding RealTime.PumpPressure, StringFormat='{}{0:F2}'}" FontSize="18"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="3" Orientation="Vertical">
|
||||
<TextBlock Text="撞击器端压力 (kPa)" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding RealTime.ImpactorPressure, StringFormat='{}{0:F2}'}" FontSize="18"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Separator Margin="0,5"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="压差: " FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding RealTime.DifferentialPressure, StringFormat='{}{0:F2} kPa'}" Foreground="DarkRed"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 主内容 (Row 2) -->
|
||||
<Grid Grid.Row="2" Margin="0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
<ColumnDefinition Width="3*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧控制区 -->
|
||||
<StackPanel Grid.Column="0" Margin="5">
|
||||
<GroupBox Header="通讯控制">
|
||||
<StackPanel>
|
||||
<Button Command="{Binding ConnectCommand}" Content="连接PLC" Width="100" Margin="5"/>
|
||||
<Button Command="{Binding DisconnectCommand}" Content="断开连接" Width="100" Margin="5"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="采样参数" Margin="0,10">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="采样时间(秒):" VerticalAlignment="Center" Width="100"/>
|
||||
<TextBox Text="{Binding SampleTimeSeconds}" Width="60" IsEnabled="{Binding IsTesting, Converter={StaticResource InverseBoolConverter}}"/>
|
||||
</StackPanel>
|
||||
<Button Command="{Binding StartTestCommand}" Content="开始测试" Width="100" Margin="5"
|
||||
IsEnabled="{Binding IsTesting, Converter={StaticResource InverseBoolConverter}}"/>
|
||||
<TextBlock Text="测试进行中..." Visibility="{Binding IsTesting, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="数据分析">
|
||||
<StackPanel>
|
||||
<Button Command="{Binding CalculateCommand}" Content="计算结果" Width="100" Margin="5"/>
|
||||
<Button Command="{Binding ExportReportCommand}" Content="导出报告" Width="100" Margin="5"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧称重数据表格 -->
|
||||
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid ItemsSource="{Binding Stages}" AutoGenerateColumns="False" CanUserAddRows="False">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="层级" Binding="{Binding StageName}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="截止直径(μm)" Binding="{Binding CutoffDiameter, StringFormat='{}{0:F1}'}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="测前质量(g)" Binding="{Binding InitialWeight, StringFormat='{}{0:F4}'}"/>
|
||||
<DataGridTextColumn Header="测后质量(g)" Binding="{Binding FinalWeight, StringFormat='{}{0:F4}'}"/>
|
||||
<DataGridTextColumn Header="净重(g)" Binding="{Binding NetWeight, StringFormat='{}{0:F6}'}" IsReadOnly="True"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- 结果显示 (Row 3) -->
|
||||
<Border Grid.Row="3" BorderBrush="Gray" BorderThickness="1" Margin="0,5,0,0" Padding="5">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="微细粒子剂量(FPD): " FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding CurrentResult.FineParticleDose, StringFormat='{}{0:F2} mg'}" Margin="5,0,20,0"/>
|
||||
<TextBlock Text="微细粒子分数(FPF): " FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding CurrentResult.FineParticleFraction, StringFormat='{}{0:F2} %'}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
43
Views/MainWindow.xaml.cs
Normal file
43
Views/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using AciTester.ViewModels;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace AciTester.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.Loaded += (s, e) =>
|
||||
{
|
||||
var keyGesture = new KeyGesture(Key.P, ModifierKeys.Control);
|
||||
var inputBinding = new InputBinding(
|
||||
new RelayCommand(OpenConfigWindow),
|
||||
keyGesture);
|
||||
this.InputBindings.Add(inputBinding);
|
||||
};
|
||||
}
|
||||
|
||||
private void OpenConfigWindow()
|
||||
{
|
||||
var vm = (MainViewModel)this.DataContext;
|
||||
var configVm = new ConfigViewModel(vm._plcService, vm._config, vm.Calibration);
|
||||
var win = new ConfigWindow { DataContext = configVm, Owner = this };
|
||||
win.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Z173药用撞击器吸入粉雾剂粒径测定仪.csproj
Normal file
17
Z173药用撞击器吸入粉雾剂粒径测定仪.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="EPPlus" Version="7.0.0" />
|
||||
<PackageReference Include="NModbus4.Core" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
3
Z173药用撞击器吸入粉雾剂粒径测定仪.slnx
Normal file
3
Z173药用撞击器吸入粉雾剂粒径测定仪.slnx
Normal file
@@ -0,0 +1,3 @@
|
||||
<Solution>
|
||||
<Project Path="Z173药用撞击器吸入粉雾剂粒径测定仪.csproj" />
|
||||
</Solution>
|
||||
Reference in New Issue
Block a user