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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/MainWindow.xaml.cs b/Views/MainWindow.xaml.cs
new file mode 100644
index 0000000..4f219d0
--- /dev/null
+++ b/Views/MainWindow.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows;
+
+namespace HME_MoistureLossMeter
+{
+ public partial class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Views/ManualControlView.xaml b/Views/ManualControlView.xaml
new file mode 100644
index 0000000..fda263f
--- /dev/null
+++ b/Views/ManualControlView.xaml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/ManualControlView.xaml.cs b/Views/ManualControlView.xaml.cs
new file mode 100644
index 0000000..282a887
--- /dev/null
+++ b/Views/ManualControlView.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace HME_MoistureLossMeter.Views
+{
+ public partial class ManualControlView : UserControl
+ {
+ public ManualControlView()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Views/RecordView.xaml b/Views/RecordView.xaml
new file mode 100644
index 0000000..67d02ed
--- /dev/null
+++ b/Views/RecordView.xaml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/RecordView.xaml.cs b/Views/RecordView.xaml.cs
new file mode 100644
index 0000000..db6ceb6
--- /dev/null
+++ b/Views/RecordView.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace HME_MoistureLossMeter.Views
+{
+ public partial class RecordView : UserControl
+ {
+ public RecordView()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Views/ReportView.xaml b/Views/ReportView.xaml
new file mode 100644
index 0000000..e180880
--- /dev/null
+++ b/Views/ReportView.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/ReportView.xaml.cs b/Views/ReportView.xaml.cs
new file mode 100644
index 0000000..f2fdf0c
--- /dev/null
+++ b/Views/ReportView.xaml.cs
@@ -0,0 +1,9 @@
+using System.Windows.Controls;
+
+namespace HME_MoistureLossMeter.Views
+{
+ public partial class ReportView : UserControl
+ {
+ public ReportView() => InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Views/TestView.xaml b/Views/TestView.xaml
new file mode 100644
index 0000000..b1be21c
--- /dev/null
+++ b/Views/TestView.xaml
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/TestView.xaml.cs b/Views/TestView.xaml.cs
new file mode 100644
index 0000000..fc36c87
--- /dev/null
+++ b/Views/TestView.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace HME_MoistureLossMeter.Views
+{
+ public partial class TestView : UserControl
+ {
+ public TestView()
+ {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..4bb1920
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,24 @@
+{
+ "PlcConfiguration": {
+ "IpAddress": "192.168.1.100",
+ "Port": 502,
+ "SlaveId": 1,
+ "PressureRegister": 1000,
+ "WetFlowRegister": 1002,
+ "WetFlowRegister2": 1004,
+ "WetFlowRegister3": 1006,
+ "PressureRegisterStation1": 1008,
+ "PressureRegisterStation2": 1010,
+ "PressureRegisterStation3": 1012
+ },
+ "MesConfiguration": {
+ "BaseUrl": "http://192.168.1.200:8080",
+ "ApiKey": "your-api-key",
+ "TimeoutSeconds": 30,
+ "RetryCount": 3
+ },
+ "DeviceConfiguration": {
+ "DeviceId": "HME-001",
+ "UpdateIntervalMs": 1000
+ }
+}
\ No newline at end of file
diff --git a/新版HME水分损失测量仪 _天平.csproj b/新版HME水分损失测量仪 _天平.csproj
new file mode 100644
index 0000000..31bdc28
--- /dev/null
+++ b/新版HME水分损失测量仪 _天平.csproj
@@ -0,0 +1,36 @@
+
+
+
+ WinExe
+ net8.0-windows
+ 新版HME水分损失测量仪__天平
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/新版HME水分损失测量仪 _天平.slnx b/新版HME水分损失测量仪 _天平.slnx
new file mode 100644
index 0000000..9e5b433
--- /dev/null
+++ b/新版HME水分损失测量仪 _天平.slnx
@@ -0,0 +1,3 @@
+
+
+