From 09072ccda553a781040e11522048ecb5a360bf73 Mon Sep 17 00:00:00 2001 From: xyy <544939200@qq.com> Date: Wed, 17 Jun 2026 15:04:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.xaml | 121 +++++ App.xaml.cs | 94 ++++ AssemblyInfo.cs | 10 + Converters/BoolToColorConverter.cs | 16 + Converters/BoolToStringConverter.cs | 15 + Converters/BoolToVisibilityConverter.cs | 39 ++ Converters/InverseBoolConverter.cs | 20 + Models/DeviceDataModel.cs | 132 ++++++ Models/PlcConfiguration.cs | 30 ++ Services/IMesService.cs | 12 + Services/IPlcService.cs | 23 + Services/MesHttpService.cs | 111 +++++ Services/ModbusTcpPlcService.cs | 232 ++++++++++ ViewModels/MainViewModel.cs | 92 ++++ ViewModels/ManualControlViewModel.cs | 176 ++++++++ ViewModels/RecordViewModel.cs | 141 ++++++ ViewModels/ReportViewModel.cs | 12 + ViewModels/TestViewModel.cs | 573 ++++++++++++++++++++++++ ViewModels/ViewModelBase.cs | 21 + Views/MainWindow.xaml | 119 +++++ Views/MainWindow.xaml.cs | 12 + Views/ManualControlView.xaml | 200 +++++++++ Views/ManualControlView.xaml.cs | 12 + Views/RecordView.xaml | 95 ++++ Views/RecordView.xaml.cs | 12 + Views/ReportView.xaml | 7 + Views/ReportView.xaml.cs | 9 + Views/TestView.xaml | 299 +++++++++++++ Views/TestView.xaml.cs | 12 + appsettings.json | 24 + 新版HME水分损失测量仪 _天平.csproj | 36 ++ 新版HME水分损失测量仪 _天平.slnx | 3 + 32 files changed, 2710 insertions(+) create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 AssemblyInfo.cs create mode 100644 Converters/BoolToColorConverter.cs create mode 100644 Converters/BoolToStringConverter.cs create mode 100644 Converters/BoolToVisibilityConverter.cs create mode 100644 Converters/InverseBoolConverter.cs create mode 100644 Models/DeviceDataModel.cs create mode 100644 Models/PlcConfiguration.cs create mode 100644 Services/IMesService.cs create mode 100644 Services/IPlcService.cs create mode 100644 Services/MesHttpService.cs create mode 100644 Services/ModbusTcpPlcService.cs create mode 100644 ViewModels/MainViewModel.cs create mode 100644 ViewModels/ManualControlViewModel.cs create mode 100644 ViewModels/RecordViewModel.cs create mode 100644 ViewModels/ReportViewModel.cs create mode 100644 ViewModels/TestViewModel.cs create mode 100644 ViewModels/ViewModelBase.cs create mode 100644 Views/MainWindow.xaml create mode 100644 Views/MainWindow.xaml.cs create mode 100644 Views/ManualControlView.xaml create mode 100644 Views/ManualControlView.xaml.cs create mode 100644 Views/RecordView.xaml create mode 100644 Views/RecordView.xaml.cs create mode 100644 Views/ReportView.xaml create mode 100644 Views/ReportView.xaml.cs create mode 100644 Views/TestView.xaml create mode 100644 Views/TestView.xaml.cs create mode 100644 appsettings.json create mode 100644 新版HME水分损失测量仪 _天平.csproj create mode 100644 新版HME水分损失测量仪 _天平.slnx diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..05bd0d9 --- /dev/null +++ b/App.xaml @@ -0,0 +1,121 @@ + + + + + #2C3E50 + #3498DB + #27AE60 + #F39C12 + #E74C3C + #ECF0F1 + #FFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..dcc4e4e --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,94 @@ +using HME_MoistureLossMeter.Models; +using HME_MoistureLossMeter.Services; +using HME_MoistureLossMeter.ViewModels; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using System; +using System.IO; +using System.Windows; + +namespace HME_MoistureLossMeter +{ + public partial class App : Application + { + private IHost _host; + public static IServiceProvider ServiceProvider { get; private set; } + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + // 配置日志 + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "log-.txt"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + + try + { + _host = Host.CreateDefaultBuilder(e.Args) + .ConfigureServices((context, services) => + { + // 配置 + var plcConfig = context.Configuration.GetSection("PlcConfiguration").Get(); + var mesConfig = context.Configuration.GetSection("MesConfiguration").Get(); + var deviceConfig = context.Configuration.GetSection("DeviceConfiguration").Get(); + + services.AddSingleton(plcConfig); + services.AddSingleton(mesConfig); + services.AddSingleton(deviceConfig); + + // 服务 + services.AddSingleton(); + services.AddSingleton(); + + // ViewModels + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + + // HttpClient + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(mesConfig.BaseUrl); + client.Timeout = TimeSpan.FromSeconds(mesConfig.TimeoutSeconds); + }); + }) + .UseSerilog() + .Build(); + + ServiceProvider = _host.Services; + + // 初始化并启动服务 + var plcService = ServiceProvider.GetRequiredService(); + var mesService = ServiceProvider.GetRequiredService(); + + // 创建主窗口 + var mainWindow = new MainWindow(); + mainWindow.DataContext = ServiceProvider.GetRequiredService(); + mainWindow.Show(); + } + catch (Exception ex) + { + Log.Fatal(ex, "应用程序启动失败"); + MessageBox.Show($"启动失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + Shutdown(); + } + } + + protected override void OnExit(ExitEventArgs e) + { + Log.Information("应用程序关闭"); + Log.CloseAndFlush(); + _host?.Dispose(); + base.OnExit(e); + } + } +} \ No newline at end of file diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/AssemblyInfo.cs @@ -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) +)] diff --git a/Converters/BoolToColorConverter.cs b/Converters/BoolToColorConverter.cs new file mode 100644 index 0000000..96341ed --- /dev/null +++ b/Converters/BoolToColorConverter.cs @@ -0,0 +1,16 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace HME_MoistureLossMeter.Converters +{ + public class BoolToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (value is bool b && b) ? Brushes.Green : Brushes.Red; + } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Converters/BoolToStringConverter.cs b/Converters/BoolToStringConverter.cs new file mode 100644 index 0000000..b599cba --- /dev/null +++ b/Converters/BoolToStringConverter.cs @@ -0,0 +1,15 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace HME_MoistureLossMeter.Converters +{ + public class BoolToStringConverter : 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(); + } +} \ No newline at end of file diff --git a/Converters/BoolToVisibilityConverter.cs b/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..c6889dd --- /dev/null +++ b/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; + +namespace HME_MoistureLossMeter.Converters +{ + public class BoolToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + if (parameter is string param) + { + // 用于颜色转换 + if (param.Contains("Brush") && targetType == typeof(Brush)) + { + var app = Application.Current; + return boolValue ? app.FindResource(param) : app.FindResource("PanelBackgroundBrush"); + } + + // 文本转换 + return boolValue ? param : ""; + } + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Visibility visibility) + return visibility == Visibility.Visible; + return false; + } + } +} \ No newline at end of file diff --git a/Converters/InverseBoolConverter.cs b/Converters/InverseBoolConverter.cs new file mode 100644 index 0000000..807b60b --- /dev/null +++ b/Converters/InverseBoolConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace HME_MoistureLossMeter.Converters +{ + public class InverseBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) return !b; + return true; + } + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) return !b; + return false; + } + } +} \ No newline at end of file diff --git a/Models/DeviceDataModel.cs b/Models/DeviceDataModel.cs new file mode 100644 index 0000000..775f0c8 --- /dev/null +++ b/Models/DeviceDataModel.cs @@ -0,0 +1,132 @@ +using System; + +namespace HME_MoistureLossMeter.Models +{ + public class DeviceDataModel + { + // 实时数据 + public float WaterTemp { get; set; } // 水浴温度 D1118 + public float ChamberTemp { get; set; } // 箱体温度 D1168 + public float Weight { get; set; } // 重量 D1268 + public float AirVolume { get; set; } // 空气体积 D4110 + public float OutletFlow { get; set; } // 出口流量 D1218 + public float TidalVolume { get; set; } // 潮气量 D300 + public float Frequency { get; set; } // 频率 D210 + public int BreathCount { get; set; } // 呼吸次数 D3000 + public float DryAirFlow { get; set; } // 干燥空气量 D3700 + + // 设置参数 + public int PresetHour { get; set; } // 设定测试时 D1681 + public int PresetMinute { get; set; } // 设定测试分 D1680 + public int PresetSecond { get; set; } // 显示时间秒 D3002 + public int DisplayHour { get; set; } // 显示时 D3004 + public int DisplayMinute { get; set; } // 显示分 D3003 + public int DisplaySecond { get; set; } // 显示秒 D3002 + + public float ManualSpeed { get; set; } // 手动速度 D218 + public float TidalCoeff { get; set; } // 潮气量系数 D302 + public float FreqCoeff { get; set; } // 频率系数 D282 + + public int OperatorId { get; set; } // 操作员编号 D3200 + public string BatchNo { get; set; } = ""; // 生产批号 D3202 + public int ExperimentId { get; set; } // 实验编号 D3204 + + // 测试结果 + public float InitialMass { get; set; } // 初始质量 D206 + public float FinalMass { get; set; } // 测后质量 D208 + public float MoistureLoss { get; set; } // 水分损失 D4106 + + // 状态 + public bool IsTesting { get; set; } + public bool IsFault { get; set; } + public bool IsHeating { get; set; } + public bool IsInhale { get; set; } + public bool IsExhale { get; set; } + public bool IsRising { get; set; } + public bool IsFalling { get; set; } + public bool IsZeroing { get; set; } + public DateTime Timestamp { get; set; } = DateTime.Now; + } + + public class TestResultModel + { + public string DeviceId { get; set; } = ""; + public string TestFinishTime { get; set; } = ""; + public string BatchNo { get; set; } = ""; + public int OperatorId { get; set; } + public int? ExperimentId { get; set; } + public float InitialMass { get; set; } + public float FinalMass { get; set; } + public float MoistureLoss { get; set; } + public TestConditions TestConditions { get; set; } = new(); + } + + public class TestConditions + { + public float? AvgWaterTemp { get; set; } + public float? AvgChamberTemp { get; set; } + public float? AvgTidalVolume { get; set; } + public float? AvgFrequency { get; set; } + public float? AvgAirVolume { get; set; } + public float? AvgOutletFlow { get; set; } + } + + public class HistoryRecordModel + { + public int? RecordId { get; set; } + public string TestTime { get; set; } = ""; + public int OperatorId { get; set; } + public string BatchNo { get; set; } = ""; + public int? ExperimentId { get; set; } + public float InitialMass { get; set; } + public float FinalMass { get; set; } + public float MoistureLoss { get; set; } + } + + public class HistoryDataModel + { + public string DeviceId { get; set; } = ""; + public List Records { get; set; } = new(); + } + + public class RealtimeDataModel + { + public string DeviceId { get; set; } = ""; + public string Timestamp { get; set; } = ""; + public RealtimeData Realtime { get; set; } = new(); + public SettingsData Settings { get; set; } = new(); + public StatusData Status { get; set; } = new(); + } + + public class RealtimeData + { + public float WaterTemp { get; set; } + public float ChamberTemp { get; set; } + public float Weight { get; set; } + public float AirVolume { get; set; } + public float OutletFlow { get; set; } + public float TidalVolume { get; set; } + public float Frequency { get; set; } + public int BreathRate { get; set; } + public float DryAirFlow { get; set; } + } + + public class SettingsData + { + public int? PresetHour { get; set; } + public int? PresetMinute { get; set; } + public int? PresetSecond { get; set; } + public float? ManualSpeed { get; set; } + public float? TidalCoeff { get; set; } + public float? FreqCoeff { get; set; } + public int? OperatorId { get; set; } + public string? BatchNo { get; set; } + public int? ExperimentId { get; set; } + } + + public class StatusData + { + public bool IsTesting { get; set; } + public bool IsFault { get; set; } + } +} \ No newline at end of file diff --git a/Models/PlcConfiguration.cs b/Models/PlcConfiguration.cs new file mode 100644 index 0000000..3f1ef06 --- /dev/null +++ b/Models/PlcConfiguration.cs @@ -0,0 +1,30 @@ +namespace HME_MoistureLossMeter.Models +{ + public class PlcConfiguration + { + public string IpAddress { get; set; } = "192.168.1.10"; + public int Port { get; set; } = 502; + public byte SlaveId { get; set; } = 1; + public ushort PressureRegister { get; set; } = 1000; + public ushort WetFlowRegister { get; set; } = 1002; + public ushort WetFlowRegister2 { get; set; } = 1004; + public ushort WetFlowRegister3 { get; set; } = 1006; + public ushort PressureRegisterStation1 { get; set; } = 1008; + public ushort PressureRegisterStation2 { get; set; } = 1010; + public ushort PressureRegisterStation3 { get; set; } = 1012; + } + + public class MesConfiguration + { + public string BaseUrl { get; set; } = "http://localhost:8080"; + public string ApiKey { get; set; } = ""; + public int TimeoutSeconds { get; set; } = 30; + public int RetryCount { get; set; } = 3; + } + + public class DeviceConfiguration + { + public string DeviceId { get; set; } = "HME-001"; + public int UpdateIntervalMs { get; set; } = 1000; + } +} \ No newline at end of file diff --git a/Services/IMesService.cs b/Services/IMesService.cs new file mode 100644 index 0000000..8f1a37d --- /dev/null +++ b/Services/IMesService.cs @@ -0,0 +1,12 @@ +using HME_MoistureLossMeter.Models; +using System.Threading.Tasks; + +namespace HME_MoistureLossMeter.Services +{ + public interface IMesService + { + Task SendRealtimeDataAsync(RealtimeDataModel data); + Task SendTestResultAsync(TestResultModel result); + Task SendHistoryDataAsync(HistoryDataModel history); + } +} \ No newline at end of file diff --git a/Services/IPlcService.cs b/Services/IPlcService.cs new file mode 100644 index 0000000..2a35119 --- /dev/null +++ b/Services/IPlcService.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace HME_MoistureLossMeter.Services +{ + public interface IPlcService + { + Task EnsureConnectedAsync(int retryCount = 3); + Task ReadFloatAsync(ushort startAddress); + Task ReadPressureAsync(); + Task ReadWetFlowAsync(int stationId); + Task ReadPressureAsync(int stationId); + Task ReadCoilAsync(ushort coilAddress); + Task ReadHoldingRegistersAsync(ushort startAddress, ushort count); + Task WriteCoilAsync(ushort coilAddress, bool value); + Task WriteRegisterAsync(ushort registerAddress, ushort value); + Task WriteSingleRegisterAsync(ushort registerAddress, ushort value); + Task WriteMultipleRegistersAsync(ushort registerAddress, float value); + Task ReadInt32Async(ushort startAddress); + Task WriteInt32Async(ushort startAddress, int value); + bool IsConnected { get; } + void Dispose(); + } +} \ No newline at end of file diff --git a/Services/MesHttpService.cs b/Services/MesHttpService.cs new file mode 100644 index 0000000..acb22fb --- /dev/null +++ b/Services/MesHttpService.cs @@ -0,0 +1,111 @@ +using HME_MoistureLossMeter.Models; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Serilog; + +namespace HME_MoistureLossMeter.Services +{ + public class MesHttpService : IMesService + { + private readonly HttpClient _httpClient; + private readonly MesConfiguration _config; + + public MesHttpService(HttpClient httpClient, MesConfiguration config) + { + _httpClient = httpClient; + _config = config; + + if (!string.IsNullOrEmpty(_config.ApiKey)) + { + _httpClient.DefaultRequestHeaders.Add("X-API-Key", _config.ApiKey); + } + } + + public async Task SendRealtimeDataAsync(RealtimeDataModel data) + { + try + { + var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/hme/realtime", content); + + if (!response.IsSuccessStatusCode) + { + Log.Warning("发送实时数据失败: {StatusCode}, {Reason}", + response.StatusCode, response.ReasonPhrase); + return false; + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "发送实时数据异常"); + return false; + } + } + + public async Task SendTestResultAsync(TestResultModel result) + { + try + { + var json = JsonConvert.SerializeObject(result, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/hme/test-result", content); + + if (!response.IsSuccessStatusCode) + { + Log.Warning("发送测试结果失败: {StatusCode}, {Reason}", + response.StatusCode, response.ReasonPhrase); + return false; + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "发送测试结果异常"); + return false; + } + } + + public async Task SendHistoryDataAsync(HistoryDataModel history) + { + try + { + var json = JsonConvert.SerializeObject(history, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/hme/history", content); + + if (!response.IsSuccessStatusCode) + { + Log.Warning("发送历史数据失败: {StatusCode}, {Reason}", + response.StatusCode, response.ReasonPhrase); + return false; + } + + return true; + } + catch (Exception ex) + { + Log.Error(ex, "发送历史数据异常"); + return false; + } + } + } +} \ No newline at end of file diff --git a/Services/ModbusTcpPlcService.cs b/Services/ModbusTcpPlcService.cs new file mode 100644 index 0000000..611fab2 --- /dev/null +++ b/Services/ModbusTcpPlcService.cs @@ -0,0 +1,232 @@ +using HME_MoistureLossMeter.Models; +using Modbus.Device; +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace HME_MoistureLossMeter.Services +{ + public class ModbusTcpPlcService : IPlcService, IDisposable + { + // 寄存器地址常量 - 测试界面 + public const ushort ADDR_WATER_TEMP = 1118; // 水浴温度 + public const ushort ADDR_CHAMBER_TEMP = 1168; // 箱体温度 + public const ushort ADDR_WEIGHT = 1268; // 重量 + public const ushort ADDR_AIR_VOLUME = 4110; // 空气体积 + public const ushort ADDR_OUTLET_FLOW = 1218; // 出口流量 + public const ushort ADDR_PRESET_HOUR = 1681; // 设定测试时 + public const ushort ADDR_PRESET_MINUTE = 1680; // 设定测试分 + public const ushort ADDR_DISPLAY_HOUR = 3004; // 显示时 + public const ushort ADDR_DISPLAY_MINUTE = 3003; // 显示分 + public const ushort ADDR_DISPLAY_SECOND = 3002; // 显示秒 + public const ushort ADDR_INITIAL_MASS = 206; // 初始质量 + public const ushort ADDR_FINAL_MASS = 208; // 测后质量 + public const ushort ADDR_MOISTURE_LOSS = 4106; // 水分损失 + public const ushort ADDR_BATCH_NO = 3202; // 生产批号 + public const ushort ADDR_OPERATOR_ID = 3200; // 操作员编号 + public const ushort ADDR_EXPERIMENT_ID = 3204; // 实验编号 + public const ushort ADDR_TIDAL_VOLUME = 300; // 潮气量 + public const ushort ADDR_FREQUENCY = 210; // 频率 + public const ushort ADDR_BREATH_COUNT = 3000; // 呼吸次数 + public const ushort ADDR_DRY_AIR_FLOW = 3700; // 通入干燥空气量 + + // 线圈地址 - 测试界面 + public const ushort COIL_RESET = 3; // 复位 M3 + public const ushort COIL_TEST = 5; // 测试 M5 + public const ushort COIL_STOP = 8; // 停止 M8 + public const ushort COIL_CLEAR = 41; // 清零 M41 + public const ushort COIL_HEAT = 300; // 加热 M300 + public const ushort COIL_P1_RECORD = 91; // P1记录 M91 + public const ushort COIL_P2_RECORD = 92; // P2记录 M92 + public const ushort COIL_PRINT = 15; // 打印 M15 + public const ushort COIL_EXHALE = 51; // 呼气 M51 + public const ushort COIL_INHALE = 55; // 吸气 M55 + public const ushort COIL_DOWN = 46; // 下降 M46 + public const ushort COIL_UP = 47; // 上升 M47 + + // 寄存器地址 - 手动界面 + public const ushort ADDR_MANUAL_SPEED = 218; // 手动速度 D218 + public const ushort ADDR_TIDAL_COEFF = 302; // 潮气量系数 D302 + public const ushort ADDR_FREQ_COEFF = 282; // 频率系数 D282 + + // 线圈地址 - 手动界面 + public const ushort COIL_LEFT = 16; // 左 M16 + public const ushort COIL_RIGHT = 17; // 右 M17 + public const ushort COIL_MANUAL_INHALE = 19; // 手动吸 M19 + public const ushort COIL_MANUAL_EXHALE = 18; // 手动呼 M18 + public const ushort COIL_ZERO = 40; // 校零 M40 + + private readonly PlcConfiguration _config; + private TcpClient _tcpClient; + private IModbusMaster _master; + + public ModbusTcpPlcService(PlcConfiguration config) + { + _config = config; + } + + public bool IsConnected => _tcpClient != null && _tcpClient.Connected; + + 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); + _master = ModbusIpMaster.CreateIp(_tcpClient); + _master.Transport.ReadTimeout = 3000; + _master.Transport.WriteTimeout = 3000; + return; + } + catch (Exception ex) when (i < retryCount - 1) + { + System.Diagnostics.Debug.WriteLine($"连接失败,500ms 后重试... {ex.Message}"); + await Task.Delay(500); + } + } + throw new Exception($"无法连接到 PLC ({_config.IpAddress}:{_config.Port}),请检查网络和 PLC 状态。"); + } + + public async Task ReadFloatAsync(ushort startAddress) + { + await EnsureConnectedAsync(); + var registers = await ReadHoldingRegistersAsync(startAddress, 2); + return UshortToFloat(registers[1], registers[0]); + } + + public async Task ReadInt32Async(ushort startAddress) + { + await EnsureConnectedAsync(); + var regs = await ReadHoldingRegistersAsync(startAddress, 2); + return regs[1] << 16 | regs[0]; + } + + public async Task ReadPressureAsync() => + await ReadFloatAsync(_config.PressureRegister); + + public async Task ReadWetFlowAsync(int stationId) + { + ushort startAddress = stationId switch + { + 1 => _config.WetFlowRegister, + 2 => _config.WetFlowRegister2, + 3 => _config.WetFlowRegister3, + _ => _config.WetFlowRegister + }; + return await ReadFloatAsync(startAddress); + } + + public async Task ReadPressureAsync(int stationId) + { + ushort startAddress = stationId switch + { + 1 => _config.PressureRegisterStation1, + 2 => _config.PressureRegisterStation2, + 3 => _config.PressureRegisterStation3, + _ => _config.PressureRegisterStation1 + }; + return await ReadFloatAsync(startAddress); + } + + public async Task ReadCoilAsync(ushort coilAddress) + { + try + { + await EnsureConnectedAsync(); + bool[] result = await _master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1); + return result[0]; + } + catch { return false; } + } + + public async Task ReadHoldingRegistersAsync(ushort startAddress, ushort count) + { + await EnsureConnectedAsync(); + return await _master.ReadHoldingRegistersAsync(_config.SlaveId, startAddress, count); + } + + 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(50); + await _master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value); + } + + public async Task WriteSingleRegisterAsync(ushort registerAddress, ushort value) + { + await EnsureConnectedAsync(); + await Task.Delay(50); + await _master.WriteSingleRegisterAsync(_config.SlaveId, registerAddress, value); + } + + public async Task WriteMultipleRegistersAsync(ushort registerAddress, float value) + { + await EnsureConnectedAsync(); + await Task.Delay(50); + await _master.WriteMultipleRegistersAsync(_config.SlaveId, registerAddress, SplitFloatToUShortArray(value)); + } + + public async Task WriteInt32Async(ushort startAddress, int value) + { + await EnsureConnectedAsync(); + ushort[] registers = ConvertIntToRegisters(value); + await _master.WriteMultipleRegistersAsync(_config.SlaveId, startAddress, registers); + } + + private 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; + } + + private 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; + } + + private ushort[] ConvertIntToRegisters(int value) + { + byte[] bytes = BitConverter.GetBytes(value); + ushort[] registers = new ushort[2]; + registers[0] = BitConverter.ToUInt16(bytes, 0); + registers[1] = BitConverter.ToUInt16(bytes, 2); + return registers; + } + + public void Dispose() + { + _master?.Dispose(); + _tcpClient?.Close(); + _tcpClient?.Dispose(); + } + } +} \ No newline at end of file diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..278245a --- /dev/null +++ b/ViewModels/MainViewModel.cs @@ -0,0 +1,92 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Windows; + +namespace HME_MoistureLossMeter.ViewModels +{ + public partial class MainViewModel : ViewModelBase + { + private readonly IServiceProvider _serviceProvider; + + [ObservableProperty] + private object _currentView; + + [ObservableProperty] + private string _currentViewName = "测试画面"; + + [ObservableProperty] + private bool _isConnected; + + [ObservableProperty] + private string _connectionStatus = "未连接"; + + [ObservableProperty] + private bool _isTesting; + + [ObservableProperty] + private DateTime _currentTime = DateTime.Now; + + public TestViewModel TestViewModel { get; } + public ManualControlViewModel ManualControlViewModel { get; } + public RecordViewModel RecordViewModel { get; } + public ReportViewModel ReportViewModel { get; } + + public MainViewModel(IServiceProvider serviceProvider, + TestViewModel testViewModel, + ManualControlViewModel manualControlViewModel, + RecordViewModel recordViewModel, + ReportViewModel reportViewModel) + { + _serviceProvider = serviceProvider; + TestViewModel = testViewModel; + ManualControlViewModel = manualControlViewModel; + RecordViewModel = recordViewModel; + ReportViewModel = reportViewModel; + + CurrentView = TestViewModel; + CurrentViewName = "测试画面"; + + var timer = new System.Timers.Timer(1000); + timer.Elapsed += (s, e) => CurrentTime = DateTime.Now; + timer.Start(); + } + + [RelayCommand] + private void NavigateToTest() + { + CurrentView = TestViewModel; + CurrentViewName = "测试画面"; + } + + [RelayCommand] + private void NavigateToManual() + { + CurrentView = ManualControlViewModel; + CurrentViewName = "手动控制"; + } + + [RelayCommand] + private void NavigateToRecord() + { + CurrentView = RecordViewModel; + CurrentViewName = "记录画面"; + } + + [RelayCommand] + private void NavigateToReport() + { + CurrentView = ReportViewModel; + CurrentViewName = "报表"; + } + + [RelayCommand] + private void ExitApplication() + { + var result = MessageBox.Show("确定要退出程序吗?", "确认退出", + MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.Yes) + Application.Current.Shutdown(); + } + } +} \ No newline at end of file diff --git a/ViewModels/ManualControlViewModel.cs b/ViewModels/ManualControlViewModel.cs new file mode 100644 index 0000000..9d08220 --- /dev/null +++ b/ViewModels/ManualControlViewModel.cs @@ -0,0 +1,176 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using HME_MoistureLossMeter.Services; +using System.Threading.Tasks; +using System.Windows; +using Serilog; + +namespace HME_MoistureLossMeter.ViewModels +{ + public partial class ManualControlViewModel : ViewModelBase + { + private readonly IPlcService _plcService; + + [ObservableProperty] + private float _manualSpeed; + + [ObservableProperty] + private float _tidalCoeff; + + [ObservableProperty] + private float _freqCoeff; + + [ObservableProperty] + private bool _isConnected; + + public ManualControlViewModel(IPlcService plcService) + { + _plcService = plcService; + + // 启动时读取参数 + Task.Run(async () => await ReadParameters()); + } + + private async Task ReadParameters() + { + try + { + if (!_plcService.IsConnected) + await _plcService.EnsureConnectedAsync(); + + ManualSpeed = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_MANUAL_SPEED); + TidalCoeff = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_TIDAL_COEFF); + FreqCoeff = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_FREQ_COEFF); + IsConnected = true; + StatusMessage = "参数读取成功"; + } + catch (Exception ex) + { + IsConnected = false; + StatusMessage = $"读取参数失败: {ex.Message}"; + Log.Error(ex, "读取手动参数失败"); + } + } + + [RelayCommand] + private async Task SaveParameters() + { + try + { + IsBusy = true; + await _plcService.WriteMultipleRegistersAsync(ModbusTcpPlcService.ADDR_MANUAL_SPEED, ManualSpeed); + await _plcService.WriteMultipleRegistersAsync(ModbusTcpPlcService.ADDR_TIDAL_COEFF, TidalCoeff); + await _plcService.WriteMultipleRegistersAsync(ModbusTcpPlcService.ADDR_FREQ_COEFF, FreqCoeff); + StatusMessage = "参数保存成功"; + Log.Information("手动参数已保存"); + } + catch (Exception ex) + { + StatusMessage = $"保存参数失败: {ex.Message}"; + Log.Error(ex, "保存手动参数失败"); + MessageBox.Show($"保存参数失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task ManualInhale() + { + await ExecuteCoilCommand(ModbusTcpPlcService.COIL_MANUAL_INHALE, "手动吸气"); + } + + [RelayCommand] + private async Task ManualExhale() + { + await ExecuteCoilCommand(ModbusTcpPlcService.COIL_MANUAL_EXHALE, "手动呼气"); + } + + [RelayCommand] + private async Task MoveLeft() + { + await ExecuteCoilCommand(ModbusTcpPlcService.COIL_LEFT, "左移"); + } + + [RelayCommand] + private async Task MoveRight() + { + await ExecuteCoilCommand(ModbusTcpPlcService.COIL_RIGHT, "右移"); + } + + [RelayCommand] + private async Task MoveUp() + { + await ExecuteCoilCommand(ModbusTcpPlcService.COIL_UP, "上升"); + } + + [RelayCommand] + private async Task MoveDown() + { + await ExecuteCoilCommand(ModbusTcpPlcService.COIL_DOWN, "下降"); + } + + [RelayCommand] + private async Task ZeroCalibration() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_ZERO, true); + StatusMessage = "校零已触发"; + Log.Information("校零触发"); + + // 等待完成后自动复位 + await Task.Delay(2000); + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_ZERO, false); + StatusMessage = "校零完成"; + } + catch (Exception ex) + { + StatusMessage = $"校零失败: {ex.Message}"; + Log.Error(ex, "校零失败"); + MessageBox.Show($"校零失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + private async Task ExecuteCoilCommand(ushort coilAddress, string commandName) + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(coilAddress, true); + StatusMessage = $"{commandName}已执行"; + Log.Information("{Command}执行", commandName); + + // 短暂延迟后自动复位 + await Task.Delay(200); + await _plcService.WriteCoilAsync(coilAddress, false); + } + catch (Exception ex) + { + StatusMessage = $"{commandName}失败: {ex.Message}"; + Log.Error(ex, "{Command}失败", commandName); + MessageBox.Show($"{commandName}失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task RefreshParameters() + { + await ReadParameters(); + } + } +} \ No newline at end of file diff --git a/ViewModels/RecordViewModel.cs b/ViewModels/RecordViewModel.cs new file mode 100644 index 0000000..cb5a514 --- /dev/null +++ b/ViewModels/RecordViewModel.cs @@ -0,0 +1,141 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using HME_MoistureLossMeter.Models; +using HME_MoistureLossMeter.Services; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using System.Windows; +using Serilog; +using System; + +namespace HME_MoistureLossMeter.ViewModels +{ + public partial class RecordViewModel : ViewModelBase + { + private readonly IMesService _mesService; + private readonly DeviceConfiguration _deviceConfig; + + [ObservableProperty] + private ObservableCollection _records = new(); + + [ObservableProperty] + private HistoryRecordModel _selectedRecord; + + public RecordViewModel(IMesService mesService, DeviceConfiguration deviceConfig) + { + _mesService = mesService; + _deviceConfig = deviceConfig; + + // 加载测试数据 + LoadTestRecords(); + } + + private void LoadTestRecords() + { + // 从本地存储或数据库加载历史记录 + // 这里使用示例数据,实际应从数据库读取 + Records.Add(new HistoryRecordModel + { + RecordId = 1, + TestTime = DateTime.Now.AddHours(-1).ToString("yyyy-MM-dd HH:mm:ss"), + OperatorId = 1001, + BatchNo = "BATCH-2024-001", + ExperimentId = 1, + InitialMass = 50.5f, + FinalMass = 48.2f, + MoistureLoss = 2.3f + }); + + Records.Add(new HistoryRecordModel + { + RecordId = 2, + TestTime = DateTime.Now.AddHours(-2).ToString("yyyy-MM-dd HH:mm:ss"), + OperatorId = 1002, + BatchNo = "BATCH-2024-002", + ExperimentId = 2, + InitialMass = 45.8f, + FinalMass = 44.1f, + MoistureLoss = 1.7f + }); + } + + [RelayCommand] + private async Task SyncToMes() + { + try + { + IsBusy = true; + StatusMessage = "正在同步历史数据到MES..."; + + var historyData = new HistoryDataModel + { + DeviceId = _deviceConfig.DeviceId, + Records = Records.ToList() + }; + + bool success = await _mesService.SendHistoryDataAsync(historyData); + + if (success) + { + StatusMessage = "历史数据同步成功"; + Log.Information("历史数据同步到MES成功"); + MessageBox.Show("历史数据同步成功", "提示", + MessageBoxButton.OK, MessageBoxImage.Information); + } + else + { + StatusMessage = "历史数据同步失败"; + MessageBox.Show("历史数据同步失败,请检查网络连接", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + catch (Exception ex) + { + StatusMessage = $"同步失败: {ex.Message}"; + Log.Error(ex, "同步历史数据到MES失败"); + MessageBox.Show($"同步失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private void ClearRecords() + { + var result = MessageBox.Show("确定要清除所有历史记录吗?", "确认清除", + MessageBoxButton.YesNo, MessageBoxImage.Warning); + + if (result == MessageBoxResult.Yes) + { + Records.Clear(); + StatusMessage = "历史记录已清除"; + Log.Information("历史记录已清除"); + } + } + + [RelayCommand] + private void DeleteSelectedRecord() + { + if (SelectedRecord == null) + { + MessageBox.Show("请先选择要删除的记录", "提示", + MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + var result = MessageBox.Show($"确定要删除记录 #{SelectedRecord.RecordId} 吗?", "确认删除", + MessageBoxButton.YesNo, MessageBoxImage.Warning); + + if (result == MessageBoxResult.Yes) + { + Records.Remove(SelectedRecord); + SelectedRecord = null; + StatusMessage = "记录已删除"; + Log.Information("记录已删除"); + } + } + } +} \ No newline at end of file diff --git a/ViewModels/ReportViewModel.cs b/ViewModels/ReportViewModel.cs new file mode 100644 index 0000000..50bab90 --- /dev/null +++ b/ViewModels/ReportViewModel.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace HME_MoistureLossMeter.ViewModels +{ + public partial class ReportViewModel : ViewModelBase + { + public ReportViewModel() + { + StatusMessage = "报表视图(待扩展)"; + } + } +} \ No newline at end of file diff --git a/ViewModels/TestViewModel.cs b/ViewModels/TestViewModel.cs new file mode 100644 index 0000000..3512ec3 --- /dev/null +++ b/ViewModels/TestViewModel.cs @@ -0,0 +1,573 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using HME_MoistureLossMeter.Models; +using HME_MoistureLossMeter.Services; +using Serilog; +using System; +using System.Collections.ObjectModel; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media.Media3D; + +namespace HME_MoistureLossMeter.ViewModels +{ + public partial class TestViewModel : ViewModelBase + { + private readonly IPlcService _plcService; + private readonly IMesService _mesService; + private readonly DeviceConfiguration _deviceConfig; + private CancellationTokenSource _cts; + private bool _isDataLoopRunning; + private float _totalWaterTemp; + private float _totalChamberTemp; + private float _totalTidalVolume; + private float _totalFrequency; + private float _totalAirVolume; + private float _totalOutletFlow; + private int _dataSampleCount; + + // 实时数据属性 + [ObservableProperty] + private float _waterTemp; + + [ObservableProperty] + private float _chamberTemp; + + [ObservableProperty] + private float _weight; + + [ObservableProperty] + private float _airVolume; + + [ObservableProperty] + private float _outletFlow; + + [ObservableProperty] + private int _presetHour; + + [ObservableProperty] + private int _presetMinute; + + [ObservableProperty] + private int _displayHour; + + [ObservableProperty] + private int _displayMinute; + + [ObservableProperty] + private int _displaySecond; + + [ObservableProperty] + private float _initialMass; + + [ObservableProperty] + private float _finalMass; + + [ObservableProperty] + private float _moistureLoss; + + [ObservableProperty] + private string _batchNo = ""; + + [ObservableProperty] + private int _operatorId; + + [ObservableProperty] + private int _experimentId; + + [ObservableProperty] + private float _tidalVolume; + + [ObservableProperty] + private float _frequency; + + [ObservableProperty] + private int _breathCount; + + [ObservableProperty] + private float _dryAirFlow; + + [ObservableProperty] + private bool _isTesting; + + [ObservableProperty] + private bool _isHeating; + + [ObservableProperty] + private bool _isInhale; + + [ObservableProperty] + private bool _isExhale; + + [ObservableProperty] + private bool _isRising; + + [ObservableProperty] + private bool _isFalling; + + [ObservableProperty] + private bool _isConnected; + + [ObservableProperty] + private string _connectionStatus = "未连接"; + + [ObservableProperty] + private DateTime _currentTime = DateTime.Now; + + public ObservableCollection TestRecords { get; } = new(); + + + [RelayCommand] + private async Task MoveUp() => await ExecuteCoilCommand(ModbusTcpPlcService.COIL_UP, "上升"); + + [RelayCommand] + private async Task MoveDown() => await ExecuteCoilCommand(ModbusTcpPlcService.COIL_DOWN, "下降"); + + [RelayCommand] + private async Task ManualInhale() => await ExecuteCoilCommand(ModbusTcpPlcService.COIL_MANUAL_INHALE, "手动吸气"); + + [RelayCommand] + private async Task ManualExhale() => await ExecuteCoilCommand(ModbusTcpPlcService.COIL_MANUAL_EXHALE, "手动呼气"); + + // 辅助方法 + private async Task ExecuteCoilCommand(ushort coilAddress, string commandName) + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(coilAddress, true); + StatusMessage = $"{commandName}已执行"; + await Task.Delay(200); + await _plcService.WriteCoilAsync(coilAddress, false); + } + catch (Exception ex) + { + StatusMessage = $"{commandName}失败: {ex.Message}"; + Log.Error(ex, "{Command}失败", commandName); + MessageBox.Show($"{commandName}失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + + + + public TestViewModel(IPlcService plcService, IMesService mesService, DeviceConfiguration deviceConfig) + { + _plcService = plcService; + _mesService = mesService; + _deviceConfig = deviceConfig; + + // 初始化定时更新 + var timer = new System.Timers.Timer(1000); + timer.Elapsed += (s, e) => CurrentTime = DateTime.Now; + timer.Start(); + } + + partial void OnIsTestingChanged(bool value) + { + if (value) + { + // 开始测试时重置数据 + _dataSampleCount = 0; + _totalWaterTemp = 0; + _totalChamberTemp = 0; + _totalTidalVolume = 0; + _totalFrequency = 0; + _totalAirVolume = 0; + _totalOutletFlow = 0; + } + } + + [RelayCommand] + private async Task Connect() + { + try + { + IsBusy = true; + StatusMessage = "正在连接PLC..."; + await _plcService.EnsureConnectedAsync(); + IsConnected = true; + ConnectionStatus = "已连接"; + StatusMessage = "PLC连接成功"; + Log.Information("PLC连接成功"); + + // 启动数据循环 + StartDataLoop(); + } + catch (Exception ex) + { + IsConnected = false; + ConnectionStatus = "连接失败"; + StatusMessage = $"连接失败: {ex.Message}"; + Log.Error(ex, "PLC连接失败"); + MessageBox.Show($"PLC连接失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private void Disconnect() + { + StopDataLoop(); + IsConnected = false; + ConnectionStatus = "已断开"; + StatusMessage = "已断开PLC连接"; + Log.Information("PLC连接已断开"); + } + + private void StartDataLoop() + { + if (_isDataLoopRunning) return; + + _cts = new CancellationTokenSource(); + _isDataLoopRunning = true; + + Task.Run(async () => + { + while (!_cts.Token.IsCancellationRequested && IsConnected) + { + try + { + await ReadAllData(); + await Task.Delay(_deviceConfig.UpdateIntervalMs, _cts.Token); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Log.Error(ex, "读取数据异常"); + StatusMessage = $"读取数据异常: {ex.Message}"; + await Task.Delay(2000); + } + } + }, _cts.Token); + } + + private void StopDataLoop() + { + _isDataLoopRunning = false; + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } + + private async Task ReadAllData() + { + try + { + // 读取浮点数数据 + WaterTemp = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_WATER_TEMP); + ChamberTemp = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_CHAMBER_TEMP); + Weight = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_WEIGHT); + AirVolume = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_AIR_VOLUME); + OutletFlow = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_OUTLET_FLOW); + TidalVolume = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_TIDAL_VOLUME); + Frequency = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_FREQUENCY); + DryAirFlow = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_DRY_AIR_FLOW); + InitialMass = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_INITIAL_MASS); + FinalMass = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_FINAL_MASS); + MoistureLoss = await _plcService.ReadFloatAsync(ModbusTcpPlcService.ADDR_MOISTURE_LOSS); + + // 读取整数数据 + PresetHour = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_PRESET_HOUR); + PresetMinute = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_PRESET_MINUTE); + DisplayHour = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_DISPLAY_HOUR); + DisplayMinute = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_DISPLAY_MINUTE); + DisplaySecond = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_DISPLAY_SECOND); + BreathCount = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_BREATH_COUNT); + OperatorId = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_OPERATOR_ID); + ExperimentId = await _plcService.ReadInt32Async(ModbusTcpPlcService.ADDR_EXPERIMENT_ID); + + // 读取字符串(批号) + var batchRegs = await _plcService.ReadHoldingRegistersAsync(ModbusTcpPlcService.ADDR_BATCH_NO, 10); + BatchNo = RegistersToString(batchRegs); + + // 读取线圈状态 + IsTesting = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_TEST); + IsHeating = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_HEAT); + IsInhale = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_INHALE); + IsExhale = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_EXHALE); + IsRising = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_UP); + IsFalling = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_DOWN); + + // 如果正在测试,收集数据用于平均值计算 + if (IsTesting) + { + _dataSampleCount++; + _totalWaterTemp += WaterTemp; + _totalChamberTemp += ChamberTemp; + _totalTidalVolume += TidalVolume; + _totalFrequency += Frequency; + _totalAirVolume += AirVolume; + _totalOutletFlow += OutletFlow; + + // 发送实时数据到MES + await SendRealtimeDataToMes(); + } + + StatusMessage = "数据读取成功"; + IsConnected = true; + ConnectionStatus = "已连接"; + } + catch (Exception ex) + { + IsConnected = false; + ConnectionStatus = "通信异常"; + Log.Error(ex, "读取数据失败"); + throw; + } + } + + private string RegistersToString(ushort[] registers) + { + var bytes = new byte[registers.Length * 2]; + for (int i = 0; i < registers.Length; i++) + { + var regBytes = BitConverter.GetBytes(registers[i]); + bytes[i * 2] = regBytes[0]; + bytes[i * 2 + 1] = regBytes[1]; + } + return System.Text.Encoding.ASCII.GetString(bytes).TrimEnd('\0'); + } + + private async Task SendRealtimeDataToMes() + { + try + { + var data = new RealtimeDataModel + { + DeviceId = _deviceConfig.DeviceId, + Timestamp = DateTime.Now.ToString("o"), + Realtime = new RealtimeData + { + WaterTemp = WaterTemp, + ChamberTemp = ChamberTemp, + Weight = Weight, + AirVolume = AirVolume, + OutletFlow = OutletFlow, + TidalVolume = TidalVolume, + Frequency = Frequency, + BreathRate = BreathCount, + DryAirFlow = DryAirFlow + }, + Settings = new SettingsData + { + PresetHour = PresetHour, + PresetMinute = PresetMinute, + PresetSecond = DisplaySecond, + OperatorId = OperatorId, + BatchNo = BatchNo, + ExperimentId = ExperimentId + }, + Status = new StatusData + { + IsTesting = IsTesting, + IsFault = false + } + }; + + await _mesService.SendRealtimeDataAsync(data); + } + catch (Exception ex) + { + Log.Error(ex, "发送实时数据到MES失败"); + } + } + + // 测试控制命令 + [RelayCommand] + private async Task StartTest() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_TEST, true); + StatusMessage = "测试已启动"; + Log.Information("测试启动"); + } + catch (Exception ex) + { + Log.Error(ex, "启动测试失败"); + MessageBox.Show($"启动测试失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task StopTest() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_STOP, true); + StatusMessage = "测试已停止"; + Log.Information("测试停止"); + } + catch (Exception ex) + { + Log.Error(ex, "停止测试失败"); + MessageBox.Show($"停止测试失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task Reset() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_RESET, true); + StatusMessage = "已复位"; + Log.Information("设备复位"); + } + catch (Exception ex) + { + Log.Error(ex, "复位失败"); + MessageBox.Show($"复位失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task Clear() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_CLEAR, true); + StatusMessage = "已清零"; + Log.Information("数据清零"); + } + catch (Exception ex) + { + Log.Error(ex, "清零失败"); + MessageBox.Show($"清零失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task ToggleHeat() + { + try + { + IsBusy = true; + bool currentState = await _plcService.ReadCoilAsync(ModbusTcpPlcService.COIL_HEAT); + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_HEAT, !currentState); + StatusMessage = currentState ? "加热关闭" : "加热开启"; + Log.Information("加热切换: {State}", !currentState); + } + catch (Exception ex) + { + Log.Error(ex, "切换加热失败"); + MessageBox.Show($"切换加热失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task RecordP1() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_P1_RECORD, true); + StatusMessage = "P1记录已触发"; + Log.Information("P1记录触发"); + } + catch (Exception ex) + { + Log.Error(ex, "P1记录失败"); + MessageBox.Show($"P1记录失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task RecordP2() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_P2_RECORD, true); + StatusMessage = "P2记录已触发"; + Log.Information("P2记录触发"); + } + catch (Exception ex) + { + Log.Error(ex, "P2记录失败"); + MessageBox.Show($"P2记录失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task Print() + { + try + { + IsBusy = true; + await _plcService.WriteCoilAsync(ModbusTcpPlcService.COIL_PRINT, true); + StatusMessage = "打印已触发"; + Log.Information("打印触发"); + } + catch (Exception ex) + { + Log.Error(ex, "打印失败"); + MessageBox.Show($"打印失败: {ex.Message}", "错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsBusy = false; + } + } + + public void Dispose() + { + StopDataLoop(); + _plcService?.Dispose(); + } + } +} \ No newline at end of file diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..a9534e6 --- /dev/null +++ b/ViewModels/ViewModelBase.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace HME_MoistureLossMeter.ViewModels +{ + public abstract class ViewModelBase : ObservableObject + { + private bool _isBusy; + public bool IsBusy + { + get => _isBusy; + set => SetProperty(ref _isBusy, value); + } + + private string _statusMessage = "就绪"; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + } +} \ No newline at end of file diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml new file mode 100644 index 0000000..3a67e47 --- /dev/null +++ b/Views/MainWindow.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +