更新modbus最新

This commit is contained in:
GukSang.Jin
2026-01-15 17:17:55 +08:00
parent 7d1af983bf
commit 1b1005c678
5 changed files with 373 additions and 20 deletions

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace COFTester.Models namespace COFTester.Models
{ {
@@ -187,6 +188,16 @@ namespace COFTester.Models
event EventHandler TestFinished; // 測試完成事件 event EventHandler TestFinished; // 測試完成事件
event EventHandler<string> ErrorOccurred; // 錯誤發生事件 event EventHandler<string> ErrorOccurred; // 錯誤發生事件
/// <summary>
/// 異步連接到設備
/// </summary>
Task<bool> ConnectAsync();
/// <summary>
/// 斷開設備連接
/// </summary>
void Disconnect();
void StartAcquisition(TestParameters parameters); void StartAcquisition(TestParameters parameters);
void StopAcquisition(); void StopAcquisition();
void ResetSensors(); void ResetSensors();

View File

@@ -185,13 +185,32 @@ namespace COFTester.Services
{ {
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
private bool _isAcquiring; private bool _isAcquiring;
private bool _isConnected;
private readonly Random _random = new Random(); private readonly Random _random = new Random();
public event EventHandler<TestDataPoint> DataReceived; public event EventHandler<TestDataPoint> DataReceived;
public event EventHandler TestFinished; public event EventHandler TestFinished;
public event EventHandler<string> ErrorOccurred; public event EventHandler<string> ErrorOccurred;
public bool IsConnected => true; // 模擬始終連接 public bool IsConnected => _isConnected;
/// <summary>
/// 模擬連接(始終成功)
/// </summary>
public Task<bool> ConnectAsync()
{
_isConnected = true;
return Task.FromResult(true);
}
/// <summary>
/// 模擬斷開連接
/// </summary>
public void Disconnect()
{
StopAcquisition();
_isConnected = false;
}
public void StartAcquisition(TestParameters parameters) public void StartAcquisition(TestParameters parameters)
{ {

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Threading; using System.Windows.Threading;
using System.Windows; using System.Windows;
using System.Threading.Tasks;
using COFTester.Models; using COFTester.Models;
using COFTester.Services; using COFTester.Services;
using COFTester.Resources; using COFTester.Resources;
@@ -15,48 +16,56 @@ using ScottPlot.WPF;
namespace COFTester.ViewModels namespace COFTester.ViewModels
{ {
public class MainViewModel : INotifyPropertyChanged public class MainViewModel : INotifyPropertyChanged, IDisposable
{ {
private readonly IDataAcquisitionService _daqService; private readonly IDataAcquisitionService _daqService;
private readonly DataProcessingService _processingService; private readonly DataProcessingService _processingService;
private readonly DispatcherTimer _clockTimer; private readonly DispatcherTimer _clockTimer;
private readonly WpfPlot _wpfPlot; private readonly WpfPlot _wpfPlot;
private readonly AppConfig _config;
private ScottPlot.Plottables.Scatter _scatterPlot = null!; private ScottPlot.Plottables.Scatter _scatterPlot = null!;
private readonly Dictionary<Guid, ScottPlot.Plottables.Scatter> _testCurves = new(); private readonly Dictionary<Guid, ScottPlot.Plottables.Scatter> _testCurves = new();
private double _currentForce; private double _currentForce;
private double _currentDisp; private double _currentDisp;
private bool _isTesting; private bool _isTesting;
private bool _isConnecting;
private TestResult? _latestResult; private TestResult? _latestResult;
private string _statusMessage; private string _statusMessage;
private DateTime _currentDateTime = DateTime.Now; private DateTime _currentDateTime = DateTime.Now;
private List<TestDataPoint> _realTimePoints; private List<TestDataPoint> _realTimePoints;
private bool _showAllCurves = true; private bool _showAllCurves = true;
private int _testCounter = 0; private int _testCounter = 0;
private bool _disposed = false;
public MainViewModel(IDataAcquisitionService daqService, DataProcessingService processingService, WpfPlot wpfPlot) public MainViewModel(IDataAcquisitionService daqService, DataProcessingService processingService, WpfPlot wpfPlot, AppConfig config)
{ {
_daqService = daqService; _daqService = daqService ?? throw new ArgumentNullException(nameof(daqService));
_processingService = processingService; _processingService = processingService ?? throw new ArgumentNullException(nameof(processingService));
_wpfPlot = wpfPlot; _wpfPlot = wpfPlot ?? throw new ArgumentNullException(nameof(wpfPlot));
_config = config ?? throw new ArgumentNullException(nameof(config));
_realTimePoints = new List<TestDataPoint>(); _realTimePoints = new List<TestDataPoint>();
// 初始化状态消息 // 初始化状态消息
_statusMessage = LanguageResources.Instance.SystemReady; _statusMessage = LanguageResources.Instance.SystemReady;
// 訂閱數據採集服務事件
_daqService.DataReceived += OnDataReceived; _daqService.DataReceived += OnDataReceived;
_daqService.TestFinished += OnTestFinished; _daqService.TestFinished += OnTestFinished;
_daqService.ErrorOccurred += (s, e) => StatusMessage = string.Format(Lang.Error, e); _daqService.ErrorOccurred += OnErrorOccurred;
StartCommand = new RelayCommand(StartTest, () => !_isTesting && _daqService.IsConnected); // 初始化命令
ConnectCommand = new AsyncRelayCommand(ConnectAsync, () => !IsConnected && !_isConnecting);
DisconnectCommand = new RelayCommand(Disconnect, () => IsConnected && !_isTesting);
StartCommand = new RelayCommand(StartTest, () => !_isTesting && IsConnected);
StopCommand = new RelayCommand(StopTest, () => _isTesting); StopCommand = new RelayCommand(StopTest, () => _isTesting);
ResetCommand = new RelayCommand(Reset, () => !_isTesting); ResetCommand = new RelayCommand(Reset, () => !_isTesting && IsConnected);
SwitchLanguageCommand = new RelayCommand(SwitchLanguage); SwitchLanguageCommand = new RelayCommand(SwitchLanguage);
OpenConfigCommand = new RelayCommand(OpenConfig); OpenConfigCommand = new RelayCommand(OpenConfig);
ClearAllTestsCommand = new RelayCommand(ClearAllTests, () => !_isTesting && TestRecords.Count > 0); ClearAllTestsCommand = new RelayCommand(ClearAllTests, () => !_isTesting && TestRecords.Count > 0);
GenerateReportCommand = new RelayCommand(GenerateReport, () => TestRecords.Count > 0); GenerateReportCommand = new RelayCommand(GenerateReport, () => TestRecords.Count > 0);
Parameters = new TestParameters(); Parameters = _config.DefaultTestParameters ?? new TestParameters();
TestRecords = new ObservableCollection<TestResult>(); TestRecords = new ObservableCollection<TestResult>();
TestRecords.CollectionChanged += (s, e) => TestRecords.CollectionChanged += (s, e) =>
{ {
@@ -74,6 +83,16 @@ namespace COFTester.ViewModels
}; };
_clockTimer.Tick += (s, e) => CurrentDateTime = DateTime.Now; _clockTimer.Tick += (s, e) => CurrentDateTime = DateTime.Now;
_clockTimer.Start(); _clockTimer.Start();
// 自動連接(如果是模擬模式則直接連接)
if (_config.CommunicationMode?.ToUpper() == "SIMULATED")
{
StatusMessage = "模擬模式 - 已就緒";
}
else
{
StatusMessage = $"通信模式: {_config.CommunicationMode} - 請點擊連接";
}
} }
/// <summary> /// <summary>
@@ -186,6 +205,49 @@ namespace COFTester.ViewModels
public TestParameters Parameters { get; set; } public TestParameters Parameters { get; set; }
/// <summary>
/// 設備連接狀態
/// </summary>
public bool IsConnected => _daqService.IsConnected;
/// <summary>
/// 是否正在連接
/// </summary>
public bool IsConnecting
{
get => _isConnecting;
set
{
_isConnecting = value;
OnPropertyChanged();
CommandManager.InvalidateRequerySuggested();
}
}
/// <summary>
/// 當前通信模式
/// </summary>
public string CommunicationMode => _config.CommunicationMode ?? "Unknown";
/// <summary>
/// 連接信息顯示
/// </summary>
public string ConnectionInfo
{
get
{
return _config.CommunicationMode?.ToUpper() switch
{
"MODBUSTCP" => $"TCP: {_config.ModbusTcp.IpAddress}:{_config.ModbusTcp.Port}",
"MODBUSRTU" => $"RTU: {_config.ModbusRtu.PortName} @ {_config.ModbusRtu.BaudRate}",
"MODBUSASCII" => $"ASCII: {_config.ModbusRtu.PortName} @ {_config.ModbusRtu.BaudRate}",
"SERIALPORT" => $"串口: {_config.SerialPort.PortName} @ {_config.SerialPort.BaudRate}",
"SIMULATED" => "模擬模式",
_ => "未知模式"
};
}
}
/// <summary> /// <summary>
/// 所有测试记录集合 /// 所有测试记录集合
/// </summary> /// </summary>
@@ -284,6 +346,8 @@ namespace COFTester.ViewModels
#endregion #endregion
#region Commands #region Commands
public ICommand ConnectCommand { get; }
public ICommand DisconnectCommand { get; }
public ICommand StartCommand { get; } public ICommand StartCommand { get; }
public ICommand StopCommand { get; } public ICommand StopCommand { get; }
public ICommand ResetCommand { get; } public ICommand ResetCommand { get; }
@@ -344,12 +408,103 @@ namespace COFTester.ViewModels
}); });
} }
private void OnErrorOccurred(object? sender, string error)
{
Application.Current?.Dispatcher.InvokeAsync(() =>
{
StatusMessage = $"錯誤: {error}";
System.Diagnostics.Debug.WriteLine($"[Modbus Error] {error}");
// 嚴重錯誤時停止測試
if (error.Contains("連接") || error.Contains("超時") || error.Contains("Connection"))
{
if (_isTesting)
{
IsTesting = false;
}
OnPropertyChanged(nameof(IsConnected));
CommandManager.InvalidateRequerySuggested();
}
});
}
#endregion #endregion
#region Actions #region Actions
/// <summary>
/// 異步連接到設備
/// </summary>
private async Task ConnectAsync()
{
if (IsConnected || IsConnecting) return;
try
{
IsConnecting = true;
StatusMessage = $"正在連接 {ConnectionInfo}...";
bool connected = await _daqService.ConnectAsync();
if (connected)
{
StatusMessage = $"已連接 - {ConnectionInfo}";
OnPropertyChanged(nameof(IsConnected));
}
else
{
StatusMessage = "連接失敗,請檢查設備和配置";
MessageBox.Show(
$"無法連接到設備\n\n通信模式: {CommunicationMode}\n連接信息: {ConnectionInfo}\n\n請檢查\n1. 設備是否已開機\n2. 連接線是否正確\n3. 配置參數是否正確",
"連接失敗",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
StatusMessage = $"連接錯誤: {ex.Message}";
MessageBox.Show($"連接時發生錯誤:\n{ex.Message}", "錯誤", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
IsConnecting = false;
CommandManager.InvalidateRequerySuggested();
}
}
/// <summary>
/// 斷開設備連接
/// </summary>
private void Disconnect()
{
try
{
if (_isTesting)
{
StopTest();
}
_daqService.Disconnect();
StatusMessage = "已斷開連接";
OnPropertyChanged(nameof(IsConnected));
CommandManager.InvalidateRequerySuggested();
}
catch (Exception ex)
{
StatusMessage = $"斷開連接錯誤: {ex.Message}";
}
}
private void StartTest() private void StartTest()
{ {
if (!IsConnected)
{
StatusMessage = "請先連接設備";
MessageBox.Show("請先連接設備後再開始測試", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
_realTimePoints.Clear(); _realTimePoints.Clear();
OnPropertyChanged(nameof(DataPointsCount)); OnPropertyChanged(nameof(DataPointsCount));
LatestResult = null; LatestResult = null;
@@ -568,6 +723,55 @@ namespace COFTester.ViewModels
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
} }
#endregion #endregion
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 停止時鐘
_clockTimer?.Stop();
// 取消訂閱事件
if (_daqService != null)
{
_daqService.DataReceived -= OnDataReceived;
_daqService.TestFinished -= OnTestFinished;
_daqService.ErrorOccurred -= OnErrorOccurred;
// 停止測試
if (_isTesting)
{
_daqService.StopAcquisition();
}
// 斷開連接
_daqService.Disconnect();
// 釋放服務資源
if (_daqService is IDisposable disposable)
{
disposable.Dispose();
}
}
}
_disposed = true;
}
~MainViewModel()
{
Dispose(false);
}
#endregion
} }
/// <summary> /// <summary>
@@ -602,4 +806,53 @@ namespace COFTester.ViewModels
// 自動觸發 CanExecuteChanged // 自動觸發 CanExecuteChanged
} }
} }
/// <summary>
/// 異步 RelayCommand 實現,用於異步操作(如連接設備)
/// </summary>
public class AsyncRelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool>? _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async void Execute(object? parameter)
{
if (!CanExecute(parameter)) return;
try
{
_isExecuting = true;
RaiseCanExecuteChanged();
await _execute();
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler? CanExecuteChanged;
private void RaiseCanExecuteChanged()
{
Application.Current?.Dispatcher.InvokeAsync(() =>
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
CommandManager.InvalidateRequerySuggested();
});
}
}
} }

View File

@@ -144,6 +144,40 @@
<Border Style="{StaticResource CardStyle}" Margin="5,10,5,5"> <Border Style="{StaticResource CardStyle}" Margin="5,10,5,5">
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Lang.TestControl}" FontWeight="Bold" Margin="0,0,0,10" Foreground="{StaticResource GrayBrush}"/> <TextBlock Text="{Binding Lang.TestControl}" FontWeight="Bold" Margin="0,0,0,10" Foreground="{StaticResource GrayBrush}"/>
<!-- 連接控制 -->
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="連接" Command="{Binding ConnectCommand}" Height="36" Foreground="White" FontSize="12" Margin="0,0,3,0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#27AE60"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</Button.Style>
</Button>
<Button Grid.Column="1" Content="斷開" Command="{Binding DisconnectCommand}" Height="36" Foreground="White" FontSize="12" Margin="3,0,0,0">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="#95A5A6"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</Button.Style>
</Button>
</Grid>
<!-- 連接狀態顯示 -->
<Border Background="#F8F9FA" CornerRadius="4" Padding="8" Margin="0,0,0,10">
<StackPanel>
<TextBlock Text="{Binding ConnectionInfo}" FontSize="11" Foreground="{StaticResource GrayBrush}" TextWrapping="Wrap"/>
</StackPanel>
</Border>
<Button Content="{Binding Lang.StartTest}" Command="{Binding StartCommand}" Height="50" Foreground="White" FontWeight="Bold" Margin="0,5" FontSize="16"> <Button Content="{Binding Lang.StartTest}" Command="{Binding StartCommand}" Height="50" Foreground="White" FontWeight="Bold" Margin="0,5" FontSize="16">
<Button.Style> <Button.Style>
<Style TargetType="Button"> <Style TargetType="Button">
@@ -331,14 +365,42 @@
<StatusBarItem> <StatusBarItem>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Ellipse Width="8" Height="8" Margin="0,0,5,0"> <Ellipse Width="8" Height="8" Margin="0,0,5,0">
<Ellipse.Fill> <Ellipse.Style>
<SolidColorBrush Color="#27AE60"/> <Style TargetType="Ellipse">
</Ellipse.Fill> <Setter Property="Fill" Value="#E74C3C"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Fill" Value="#27AE60"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse> </Ellipse>
<TextBlock Text="{Binding Lang.PLCConnected}" Foreground="{StaticResource SuccessBrush}"/> <TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="未連接"/>
<Setter Property="Foreground" Value="#E74C3C"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="已連接"/>
<Setter Property="Foreground" Value="#27AE60"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsConnecting}" Value="True">
<Setter Property="Text" Value="連接中..."/>
<Setter Property="Foreground" Value="#F39C12"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel> </StackPanel>
</StatusBarItem> </StatusBarItem>
<Separator/> <Separator/>
<StatusBarItem>
<TextBlock Text="{Binding CommunicationMode}" Foreground="{StaticResource GrayBrush}" FontSize="11"/>
</StatusBarItem>
<Separator/>
<StatusBarItem> <StatusBarItem>
<TextBlock Foreground="{StaticResource GrayBrush}"> <TextBlock Foreground="{StaticResource GrayBrush}">
<Run Text="{Binding Lang.DataPointsCount, Mode=OneWay}"/> <Run Text="{Binding Lang.DataPointsCount, Mode=OneWay}"/>

View File

@@ -1,5 +1,6 @@
using COFTester.ViewModels; using COFTester.ViewModels;
using COFTester.Services; using COFTester.Services;
using COFTester.Models;
using System.Windows; using System.Windows;
namespace COFTester.Views namespace COFTester.Views
@@ -9,22 +10,29 @@ namespace COFTester.Views
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly MainViewModel _viewModel;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
// 實例化服務 // 加載配置
var daqService = new SimulatedDataAcquisitionService(); var config = AppConfig.Load(AppConfig.GetDefaultConfigPath());
// 根據配置創建數據採集服務(支持 Modbus TCP/RTU/ASCII 和模擬模式)
IDataAcquisitionService daqService = ModbusServiceFactory.CreateService(config);
var processingService = new DataProcessingService(); var processingService = new DataProcessingService();
// 設置 DataContext // 設置 DataContext
var viewModel = new MainViewModel(daqService, processingService, FrictionPlot); _viewModel = new MainViewModel(daqService, processingService, FrictionPlot, config);
this.DataContext = viewModel; this.DataContext = _viewModel;
} }
private void Button_Click(object sender, RoutedEventArgs e) protected override void OnClosed(System.EventArgs e)
{ {
// 清理資源
_viewModel?.Dispose();
base.OnClosed(e);
} }
} }
} }