添加项目文件。

This commit is contained in:
xyy
2026-06-17 15:04:35 +08:00
parent ba1c916dd7
commit 09072ccda5
32 changed files with 2710 additions and 0 deletions

121
App.xaml Normal file
View File

@@ -0,0 +1,121 @@
<Application x:Class="HME_MoistureLossMeter.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HME_MoistureLossMeter"
xmlns:converters="clr-namespace:HME_MoistureLossMeter.Converters"
xmlns:vm="clr-namespace:HME_MoistureLossMeter.ViewModels"
xmlns:views="clr-namespace:HME_MoistureLossMeter.Views"
>
<Application.Resources>
<ResourceDictionary>
<!-- 颜色资源 -->
<Color x:Key="PrimaryColor">#2C3E50</Color>
<Color x:Key="AccentColor">#3498DB</Color>
<Color x:Key="SuccessColor">#27AE60</Color>
<Color x:Key="WarningColor">#F39C12</Color>
<Color x:Key="DangerColor">#E74C3C</Color>
<Color x:Key="BackgroundColor">#ECF0F1</Color>
<Color x:Key="PanelBackgroundColor">#FFFFFF</Color>
<!-- 画刷 -->
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}"/>
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource SuccessColor}"/>
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource WarningColor}"/>
<SolidColorBrush x:Key="DangerBrush" Color="{StaticResource DangerColor}"/>
<SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}"/>
<SolidColorBrush x:Key="PanelBackgroundBrush" Color="{StaticResource PanelBackgroundColor}"/>
<!-- 转换器 -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<!-- 样式 -->
<Style TargetType="Button">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2980B9"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1A5276"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#BDC3C7"/>
<Setter Property="Foreground" Value="#7F8C8D"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="TextBox">
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="BorderBrush" Value="#BDC3C7"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}"/>
</Style>
<Style TargetType="Label">
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBlock" x:Key="DisplayValueStyle">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
</Style>
<Style TargetType="TextBlock" x:Key="DisplayLabelStyle">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#7F8C8D"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
<Style TargetType="Border" x:Key="PanelBorderStyle">
<Setter Property="Background" Value="{StaticResource PanelBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="#E0E0E0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="8"/>
</Style>
<DataTemplate DataType="{x:Type vm:TestViewModel}">
<views:TestView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ManualControlViewModel}">
<views:ManualControlView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:RecordViewModel}">
<views:RecordView />
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>

94
App.xaml.cs Normal file
View File

@@ -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<PlcConfiguration>();
var mesConfig = context.Configuration.GetSection("MesConfiguration").Get<MesConfiguration>();
var deviceConfig = context.Configuration.GetSection("DeviceConfiguration").Get<DeviceConfiguration>();
services.AddSingleton(plcConfig);
services.AddSingleton(mesConfig);
services.AddSingleton(deviceConfig);
// 服务
services.AddSingleton<IPlcService, ModbusTcpPlcService>();
services.AddSingleton<IMesService, MesHttpService>();
// ViewModels
services.AddSingleton<MainViewModel>();
services.AddSingleton<TestViewModel>();
services.AddSingleton<ManualControlViewModel>();
services.AddSingleton<RecordViewModel>();
services.AddSingleton<ReportViewModel>();
// HttpClient
services.AddHttpClient<IMesService, MesHttpService>(client =>
{
client.BaseAddress = new Uri(mesConfig.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(mesConfig.TimeoutSeconds);
});
})
.UseSerilog()
.Build();
ServiceProvider = _host.Services;
// 初始化并启动服务
var plcService = ServiceProvider.GetRequiredService<IPlcService>();
var mesService = ServiceProvider.GetRequiredService<IMesService>();
// 创建主窗口
var mainWindow = new MainWindow();
mainWindow.DataContext = ServiceProvider.GetRequiredService<MainViewModel>();
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);
}
}
}

10
AssemblyInfo.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

132
Models/DeviceDataModel.cs Normal file
View File

@@ -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<HistoryRecordModel> 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; }
}
}

View File

@@ -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;
}
}

12
Services/IMesService.cs Normal file
View File

@@ -0,0 +1,12 @@
using HME_MoistureLossMeter.Models;
using System.Threading.Tasks;
namespace HME_MoistureLossMeter.Services
{
public interface IMesService
{
Task<bool> SendRealtimeDataAsync(RealtimeDataModel data);
Task<bool> SendTestResultAsync(TestResultModel result);
Task<bool> SendHistoryDataAsync(HistoryDataModel history);
}
}

23
Services/IPlcService.cs Normal file
View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace HME_MoistureLossMeter.Services
{
public interface IPlcService
{
Task EnsureConnectedAsync(int retryCount = 3);
Task<float> ReadFloatAsync(ushort startAddress);
Task<float> ReadPressureAsync();
Task<float> ReadWetFlowAsync(int stationId);
Task<float> ReadPressureAsync(int stationId);
Task<bool> ReadCoilAsync(ushort coilAddress);
Task<ushort[]> 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<int> ReadInt32Async(ushort startAddress);
Task WriteInt32Async(ushort startAddress, int value);
bool IsConnected { get; }
void Dispose();
}
}

111
Services/MesHttpService.cs Normal file
View File

@@ -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<bool> 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<bool> 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<bool> 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;
}
}
}
}

View File

@@ -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<float> ReadFloatAsync(ushort startAddress)
{
await EnsureConnectedAsync();
var registers = await ReadHoldingRegistersAsync(startAddress, 2);
return UshortToFloat(registers[1], registers[0]);
}
public async Task<int> ReadInt32Async(ushort startAddress)
{
await EnsureConnectedAsync();
var regs = await ReadHoldingRegistersAsync(startAddress, 2);
return regs[1] << 16 | regs[0];
}
public async Task<float> ReadPressureAsync() =>
await ReadFloatAsync(_config.PressureRegister);
public async Task<float> ReadWetFlowAsync(int stationId)
{
ushort startAddress = stationId switch
{
1 => _config.WetFlowRegister,
2 => _config.WetFlowRegister2,
3 => _config.WetFlowRegister3,
_ => _config.WetFlowRegister
};
return await ReadFloatAsync(startAddress);
}
public async Task<float> 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<bool> ReadCoilAsync(ushort coilAddress)
{
try
{
await EnsureConnectedAsync();
bool[] result = await _master.ReadCoilsAsync(_config.SlaveId, coilAddress, 1);
return result[0];
}
catch { return false; }
}
public async Task<ushort[]> 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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<HistoryRecordModel> _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("记录已删除");
}
}
}
}

View File

@@ -0,0 +1,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace HME_MoistureLossMeter.ViewModels
{
public partial class ReportViewModel : ViewModelBase
{
public ReportViewModel()
{
StatusMessage = "报表视图(待扩展)";
}
}
}

573
ViewModels/TestViewModel.cs Normal file
View File

@@ -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<HistoryRecordModel> 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();
}
}
}

View File

@@ -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);
}
}
}

119
Views/MainWindow.xaml Normal file
View File

@@ -0,0 +1,119 @@
<Window x:Class="HME_MoistureLossMeter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:HME_MoistureLossMeter.Views"
xmlns:converters="clr-namespace:HME_MoistureLossMeter.Converters"
Title="HME水分损失测量仪"
Width="1024" Height="768"
WindowStartupLocation="CenterScreen"
Background="#F0F2F5">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Height" Value="40"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Background" Value="#2C7DA0"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#1F5E7A"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Cursor" Value="Hand"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#CCCCCC"/>
<Setter Property="Foreground" Value="#666666"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1F5E7A"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox">
<Setter Property="Height" Value="35"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderBrush" Value="#CCCCCC"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="5"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="GroupBox">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0,5"/>
</Style>
<Style TargetType="Border" x:Key="CardBorder">
<Setter Property="Background" Value="White"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="BorderBrush" Value="#DDDDDD"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:BoolToColorConverter x:Key="BoolToColorConverter"/>
<converters:BoolToStringConverter x:Key="BoolToStringConverter"/>
<converters:InverseBoolConverter x:Key="InverseBoolConverter"/>
</Window.Resources>
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 顶部状态栏 -->
<Border Grid.Row="0" Background="#2C3E50" CornerRadius="5" Padding="10" Margin="0,0,0,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="状态:" Foreground="White" FontSize="14" VerticalAlignment="Center"/>
<TextBlock Text="{Binding ConnectionStatus}"
Foreground="{Binding IsConnected, Converter={StaticResource BoolToColorConverter}}"
FontSize="14" FontWeight="Bold" Margin="5,0,20,0"/>
<Separator Background="White" Width="1" Margin="5,0"/>
<TextBlock Text="当前视图:" Foreground="White" FontSize="14" Margin="15,0,0,0"/>
<TextBlock Text="{Binding CurrentViewName}" Foreground="White" FontSize="14" FontWeight="Bold" Margin="5,0,20,0"/>
<Separator Background="White" Width="1" Margin="5,0"/>
<TextBlock Text="测试状态:" Foreground="White" FontSize="14" Margin="15,0,0,0"/>
<TextBlock Text="{Binding IsTesting, Converter={StaticResource BoolToStringConverter}}"
Foreground="White" FontSize="14" FontWeight="Bold" Margin="5,0,0,0"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding CurrentTime, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}"
Foreground="White" FontSize="14" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- 主内容 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 导航 -->
<Border Grid.Column="0" Style="{StaticResource CardBorder}" Margin="0,0,10,0">
<StackPanel Margin="5">
<Button Content="测试画面" Command="{Binding NavigateToTestCommand}" Height="45" Margin="0,5"/>
<Button Content="手动控制" Command="{Binding NavigateToManualCommand}" Height="45" Margin="0,5"/>
<Button Content="记录画面" Command="{Binding NavigateToRecordCommand}" Height="45" Margin="0,5"/>
<Button Content="报表" Command="{Binding NavigateToReportCommand}" Height="45" Margin="0,5"/>
<Separator Margin="0,15"/>
<Button Content="退出" Command="{Binding ExitApplicationCommand}" Height="45" Margin="0,5" Background="#E74C3C"/>
</StackPanel>
</Border>
<!-- 视图 -->
<ContentControl Grid.Column="1" Content="{Binding CurrentView}"/>
</Grid>
<!-- 底部状态 -->
<Border Grid.Row="2" Background="#2C3E50" CornerRadius="5" Padding="5" Margin="0,10,0,0">
<TextBlock Text="{Binding StatusMessage}" Foreground="White" FontSize="12" HorizontalAlignment="Center"/>
</Border>
</Grid>
</Window>

12
Views/MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,12 @@
using System.Windows;
namespace HME_MoistureLossMeter
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,200 @@
<UserControl x:Class="HME_MoistureLossMeter.Views.ManualControlView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="700" d:DesignWidth="1024">
<Grid Background="{StaticResource BackgroundBrush}" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 左侧:参数设置 -->
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="0"
Style="{StaticResource PanelBorderStyle}"
Margin="0,0,10,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="参数设置"
FontSize="16"
FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}"
Margin="0,0,0,15"/>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="手动速度"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding ManualSpeed, StringFormat='{}{0:F2}'}"
Margin="5,0,5,0"/>
<TextBlock Grid.Row="0" Grid.Column="2"
Text="mm/MIN"
VerticalAlignment="Center"
Foreground="#7F8C8D"/>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="潮气量系数"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1"
Text="{Binding TidalCoeff, StringFormat='{}{0:F2}'}"
Margin="5,0,5,0"/>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="频率系数"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1"
Text="{Binding FreqCoeff, StringFormat='{}{0:F2}'}"
Margin="5,0,5,0"/>
<StackPanel Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0,10,0,0">
<Button Content="保存参数"
Command="{Binding SaveParametersCommand}"
Width="100" Height="35"
Background="{StaticResource SuccessBrush}"/>
<Button Content="刷新"
Command="{Binding RefreshParametersCommand}"
Width="80" Height="35"
Margin="10,0,0,0"/>
</StackPanel>
</Grid>
</Grid>
</Border>
<!-- 右侧:手动控制 -->
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
Style="{StaticResource PanelBorderStyle}"
Margin="10,0,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="手动控制"
FontSize="16"
FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}"
Margin="0,0,0,15"/>
<!-- 控制按钮 -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 方向控制 -->
<Button Grid.Row="0" Grid.Column="1"
Content="▲ 上升"
Command="{Binding MoveUpCommand}"
Height="40"
Background="{StaticResource WarningBrush}"
Margin="5"/>
<Button Grid.Row="1" Grid.Column="0"
Content="◄ 左"
Command="{Binding MoveLeftCommand}"
Height="40"
Margin="5"/>
<Button Grid.Row="1" Grid.Column="1"
Content="校零"
Command="{Binding ZeroCalibrationCommand}"
Height="40"
Background="{StaticResource AccentBrush}"
Margin="5"/>
<Button Grid.Row="1" Grid.Column="2"
Content="右 ►"
Command="{Binding MoveRightCommand}"
Height="40"
Margin="5"/>
<Button Grid.Row="2" Grid.Column="1"
Content="▼ 下降"
Command="{Binding MoveDownCommand}"
Height="40"
Background="{StaticResource WarningBrush}"
Margin="5"/>
<!-- 呼吸控制 -->
<Button Grid.Row="3" Grid.Column="0"
Content="手动呼"
Command="{Binding ManualExhaleCommand}"
Height="40"
Background="{StaticResource DangerBrush}"
Margin="5"/>
<Button Grid.Row="3" Grid.Column="1"
Content="停止"
Command="{Binding StopTestCommand}"
Height="40"
Background="{StaticResource DangerBrush}"
Margin="5"/>
<Button Grid.Row="3" Grid.Column="2"
Content="手动吸"
Command="{Binding ManualInhaleCommand}"
Height="40"
Background="{StaticResource SuccessBrush}"
Margin="5"/>
</Grid>
<!-- 状态显示 -->
<Border Grid.Row="2" Background="#F8F9FA" CornerRadius="4" Padding="5" Margin="0,10,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="PLC状态: " FontWeight="Bold" FontSize="12"/>
<TextBlock Text="{Binding ConnectionStatus}"
Foreground="{Binding IsConnected, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=SuccessBrush, FallbackValue={StaticResource DangerBrush}}"
FontWeight="Bold"/>
<TextBlock Text=" | 手动速度: " FontWeight="Bold" FontSize="12" Margin="15,0,0,0"/>
<TextBlock Text="{Binding ManualSpeed, StringFormat='{}{0:F2} mm/MIN'}" FontSize="12"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace HME_MoistureLossMeter.Views
{
public partial class ManualControlView : UserControl
{
public ManualControlView()
{
InitializeComponent();
}
}
}

95
Views/RecordView.xaml Normal file
View File

@@ -0,0 +1,95 @@
<UserControl x:Class="HME_MoistureLossMeter.Views.RecordView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="700" d:DesignWidth="1024">
<Grid Background="{StaticResource BackgroundBrush}" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0"
Text="历史记录"
FontSize="18"
FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}"
Margin="0,0,0,10"/>
<!-- 数据表格 -->
<Border Grid.Row="1"
Style="{StaticResource PanelBorderStyle}">
<DataGrid ItemsSource="{Binding Records}"
SelectedItem="{Binding SelectedRecord}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
IsReadOnly="True"
HeadersVisibility="Column"
RowHeight="35"
FontSize="13"
AlternatingRowBackground="#F8F9FA"
GridLinesVisibility="Horizontal"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<DataGrid.Resources>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Height" Value="35"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="编号" Binding="{Binding RecordId}" Width="80"/>
<DataGridTextColumn Header="时间" Binding="{Binding TestTime}" Width="180"/>
<DataGridTextColumn Header="操作员编号" Binding="{Binding OperatorId}" Width="120"/>
<DataGridTextColumn Header="生产批号" Binding="{Binding BatchNo}" Width="150"/>
<DataGridTextColumn Header="实验编号" Binding="{Binding ExperimentId}" Width="100"/>
<DataGridTextColumn Header="初始质量(g)" Binding="{Binding InitialMass, StringFormat='{}{0:F2}'}" Width="120"/>
<DataGridTextColumn Header="测后质量(g)" Binding="{Binding FinalMass, StringFormat='{}{0:F2}'}" Width="120"/>
<DataGridTextColumn Header="水分损失(mg/L)" Binding="{Binding MoistureLoss, StringFormat='{}{0:F2}'}" Width="140">
<DataGridTextColumn.ElementStyle>
<Style>
<Setter Property="TextBlock.Foreground" Value="{StaticResource DangerBrush}"/>
<Setter Property="TextBlock.FontWeight" Value="Bold"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- 底部操作按钮 -->
<WrapPanel Grid.Row="2"
HorizontalAlignment="Center"
Margin="0,10,0,0">
<Button Content="同步到MES"
Command="{Binding SyncToMesCommand}"
Width="120" Height="35"
Background="{StaticResource SuccessBrush}"/>
<Button Content="删除选中"
Command="{Binding DeleteSelectedRecordCommand}"
Width="120" Height="35"
Background="{StaticResource DangerBrush}"
Margin="10,0,0,0"/>
<Button Content="清除全部"
Command="{Binding ClearRecordsCommand}"
Width="120" Height="35"
Background="{StaticResource WarningBrush}"
Margin="10,0,0,0"/>
<TextBlock Text="提示:长按可清除记录"
Foreground="#7F8C8D"
FontSize="12"
VerticalAlignment="Center"
Margin="20,0,0,0"/>
</WrapPanel>
</Grid>
</UserControl>

12
Views/RecordView.xaml.cs Normal file
View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace HME_MoistureLossMeter.Views
{
public partial class RecordView : UserControl
{
public RecordView()
{
InitializeComponent();
}
}
}

7
Views/ReportView.xaml Normal file
View File

@@ -0,0 +1,7 @@
<UserControl x:Class="HME_MoistureLossMeter.Views.ReportView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Background="White">
<TextBlock Text="报表功能开发中..." FontSize="24" Foreground="Gray" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</UserControl>

9
Views/ReportView.xaml.cs Normal file
View File

@@ -0,0 +1,9 @@
using System.Windows.Controls;
namespace HME_MoistureLossMeter.Views
{
public partial class ReportView : UserControl
{
public ReportView() => InitializeComponent();
}
}

299
Views/TestView.xaml Normal file
View File

@@ -0,0 +1,299 @@
<UserControl x:Class="HME_MoistureLossMeter.Views.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="700" d:DesignWidth="1024">
<Grid Background="{StaticResource BackgroundBrush}" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 左侧:主显示区域 -->
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="0"
Style="{StaticResource PanelBorderStyle}"
Margin="0,0,10,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Text="实时数据"
FontSize="16"
FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}"
Margin="0,0,0,10"/>
<!-- 数据显示网格 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 第一列 -->
<StackPanel Grid.Row="0" Grid.Column="0" Margin="5">
<TextBlock Text="水浴温度" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding WaterTemp, StringFormat='{}{0:F2} ℃'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Margin="5">
<TextBlock Text="箱体温度" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding ChamberTemp, StringFormat='{}{0:F2} ℃'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Margin="5">
<TextBlock Text="重量" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding Weight, StringFormat='{}{0:F2} g'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="3" Grid.Column="0" Margin="5">
<TextBlock Text="空气体积" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding AirVolume, StringFormat='{}{0:F2} L'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="4" Grid.Column="0" Margin="5">
<TextBlock Text="出口流量" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding OutletFlow, StringFormat='{}{0:F2} L/min'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="5" Grid.Column="0" Margin="5">
<TextBlock Text="潮气量" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding TidalVolume, StringFormat='{}{0:F1} ml'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="6" Grid.Column="0" Margin="5">
<TextBlock Text="频率" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding Frequency, StringFormat='{}{0:F2} 次/min'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="7" Grid.Column="0" Margin="5">
<TextBlock Text="呼吸次数" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding BreathCount}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<!-- 第二列 -->
<StackPanel Grid.Row="0" Grid.Column="1" Margin="5">
<TextBlock Text="设定测试时间" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding PresetHour, StringFormat='{}{0}H'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" Margin="5">
<TextBlock Text="当前时间" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding CurrentTime, StringFormat='{}{0:HH:mm:ss}'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="1" Margin="5">
<TextBlock Text="初始质量" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding InitialMass, StringFormat='{}{0:F2} g'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="3" Grid.Column="1" Margin="5">
<TextBlock Text="测后质量" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding FinalMass, StringFormat='{}{0:F2} g'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="4" Grid.Column="1" Margin="5">
<TextBlock Text="水分损失" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding MoistureLoss, StringFormat='{}{0:F2} mg/L'}"
Style="{StaticResource DisplayValueStyle}"
Foreground="{StaticResource DangerBrush}"/>
</StackPanel>
<StackPanel Grid.Row="5" Grid.Column="1" Margin="5">
<TextBlock Text="干燥空气量" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding DryAirFlow, StringFormat='{}{0:F2} L/min'}"
Style="{StaticResource DisplayValueStyle}"/>
</StackPanel>
<StackPanel Grid.Row="6" Grid.Column="1" Margin="5">
<TextBlock Text="测试状态" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding IsTesting, Converter={StaticResource BoolToVisibilityConverter},
FallbackValue=未测试}"
Style="{StaticResource DisplayValueStyle}"
Foreground="{Binding IsTesting, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=SuccessBrush, FallbackValue={StaticResource WarningBrush}}"/>
</StackPanel>
<!-- 第三列:状态指示灯 -->
<StackPanel Grid.Row="0" Grid.Column="2" Margin="5">
<TextBlock Text="状态指示灯" Style="{StaticResource DisplayLabelStyle}"/>
<WrapPanel Margin="0,5,0,0">
<Border Background="{Binding IsHeating, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=WarningBrush, FallbackValue=#95A5A6}"
Width="20" Height="20" CornerRadius="10" Margin="2"/>
<TextBlock Text="加热" FontSize="11" VerticalAlignment="Center" Margin="2,0,10,0"/>
<Border Background="{Binding IsInhale, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=SuccessBrush, FallbackValue=#95A5A6}"
Width="20" Height="20" CornerRadius="10" Margin="2"/>
<TextBlock Text="吸气" FontSize="11" VerticalAlignment="Center" Margin="2,0,10,0"/>
<Border Background="{Binding IsExhale, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=DangerBrush, FallbackValue=#95A5A6}"
Width="20" Height="20" CornerRadius="10" Margin="2"/>
<TextBlock Text="呼气" FontSize="11" VerticalAlignment="Center" Margin="2,0,10,0"/>
</WrapPanel>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="2" Margin="5">
<TextBlock Text="PLC状态" Style="{StaticResource DisplayLabelStyle}"/>
<TextBlock Text="{Binding ConnectionStatus}"
Style="{StaticResource DisplayValueStyle}"
Foreground="{Binding IsConnected, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=SuccessBrush, FallbackValue={StaticResource DangerBrush}}"/>
</StackPanel>
</Grid>
<!-- 底部信息 -->
<Border Grid.Row="2" Background="#F8F9FA" CornerRadius="4" Padding="5" Margin="0,10,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="实验条件: " FontWeight="Bold" FontSize="12"/>
<TextBlock Text="{Binding CurrentTime, StringFormat='{}{0:MM/dd/yy ddd HH:mm:ss}'}" FontSize="12"/>
<TextBlock Text=" | 操作员: " FontWeight="Bold" FontSize="12" Margin="10,0,0,0"/>
<TextBlock Text="{Binding OperatorId}" FontSize="12"/>
<TextBlock Text=" | 批号: " FontWeight="Bold" FontSize="12" Margin="10,0,0,0"/>
<TextBlock Text="{Binding BatchNo}" FontSize="12"/>
<TextBlock Text=" | 实验编号: " FontWeight="Bold" FontSize="12" Margin="10,0,0,0"/>
<TextBlock Text="{Binding ExperimentId}" FontSize="12"/>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- 右侧:控制区域 -->
<Border Grid.Row="0" Grid.Column="1"
Style="{StaticResource PanelBorderStyle}"
Margin="10,0,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="测试控制"
FontSize="16"
FontWeight="Bold"
Foreground="{StaticResource PrimaryBrush}"
Margin="0,0,0,10"/>
<WrapPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,10,0,0">
<Button Content="复位"
Command="{Binding ResetCommand}"
Width="80" Height="40"
Background="{StaticResource WarningBrush}"/>
<Button Content="测试"
Command="{Binding StartTestCommand}"
Width="80" Height="40"
Background="{StaticResource SuccessBrush}"/>
<Button Content="停止"
Command="{Binding StopTestCommand}"
Width="80" Height="40"
Background="{StaticResource DangerBrush}"/>
<Button Content="P1记录"
Command="{Binding RecordP1Command}"
Width="80" Height="40"/>
<Button Content="P2记录"
Command="{Binding RecordP2Command}"
Width="80" Height="40"/>
<Button Content="打印"
Command="{Binding PrintCommand}"
Width="80" Height="40"/>
<Button Content="加热"
Command="{Binding ToggleHeatCommand}"
Width="80" Height="40"
Background="{Binding IsHeating, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=DangerBrush, FallbackValue={StaticResource AccentBrush}}"/>
<Button Content="呼气"
Command="{Binding ManualExhaleCommand}"
Width="80" Height="40"
Background="{StaticResource DangerBrush}"/>
<Button Content="吸气"
Command="{Binding ManualInhaleCommand}"
Width="80" Height="40"
Background="{StaticResource SuccessBrush}"/>
<Button Content="下降"
Command="{Binding MoveDownCommand}"
Width="80" Height="40"
Background="{StaticResource WarningBrush}"/>
<Button Content="上升"
Command="{Binding MoveUpCommand}"
Width="80" Height="40"
Background="{StaticResource WarningBrush}"/>
<Button Content="清零"
Command="{Binding ClearCommand}"
Width="80" Height="40"/>
</WrapPanel>
</Grid>
</Border>
<!-- 连接控制 -->
<Border Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
Style="{StaticResource PanelBorderStyle}"
Margin="0,10,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="连接PLC"
Command="{Binding ConnectCommand}"
Width="100" Height="35"
Background="{StaticResource SuccessBrush}"/>
<Button Content="断开PLC"
Command="{Binding DisconnectCommand}"
Width="100" Height="35"
Background="{StaticResource DangerBrush}"
Margin="10,0,0,0"/>
<TextBlock Text="状态:" FontWeight="Bold" Margin="20,0,5,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding ConnectionStatus}"
Foreground="{Binding IsConnected, Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=SuccessBrush, FallbackValue={StaticResource DangerBrush}}"
VerticalAlignment="Center"
FontWeight="Bold"/>
</StackPanel>
</Border>
</Grid>
</UserControl>

12
Views/TestView.xaml.cs Normal file
View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace HME_MoistureLossMeter.Views
{
public partial class TestView : UserControl
{
public TestView()
{
InitializeComponent();
}
}
}

24
appsettings.json Normal file
View File

@@ -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
}
}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>新版HME水分损失测量仪__天平</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Folder Include="Helpers\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="LiveCharts.Wpf" Version="0.9.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="NModbus4.Core" Version="1.0.2" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="新版HME水分损失测量仪 _天平.csproj" />
</Solution>