Files
XplorePlane/XP.Hardware.PLC/ViewModels/PlcTestBenchViewModel.cs
T

716 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Prism.Commands;
using Prism.Mvvm;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Plc.Abstractions;
using XP.Hardware.Plc.Exceptions;
using XP.Hardware.Plc.Services;
using XP.Hardware.PLC.Configs;
using XP.Hardware.PLC.Helpers;
namespace XP.Hardware.PLC.ViewModels
{
/// <summary>
/// PLC 测试台 ViewModel | PLC Test Bench ViewModel
/// 提供连接管理、单点读写、批量读取和日志功能
/// </summary>
public class PlcTestBenchViewModel : BindableBase
{
private readonly PlcService _plcService;
private readonly IPlcClient _plcClient;
private readonly ConfigLoader _configLoader;
private readonly ILoggerService _logger;
private CancellationTokenSource _cts;
private System.Windows.Threading.DispatcherTimer _continuousReadTimer;
// === 数据类型选项 | Data type options ===
private readonly string[] _dataTypes = { "Bool", "Byte", "Short", "Int", "Float", "Double", "String" };
public string[] DataTypes => _dataTypes;
// === 连接配置区属性 | Connection config properties ===
private string _ipAddress = "127.0.0.1";
public string IpAddress
{
get => _ipAddress;
set => SetProperty(ref _ipAddress, value);
}
private int _port = 502;
public int Port
{
get => _port;
set => SetProperty(ref _port, value);
}
private short _rack = 0;
public short Rack
{
get => _rack;
set => SetProperty(ref _rack, value);
}
private short _slot = 1;
public short Slot
{
get => _slot;
set => SetProperty(ref _slot, value);
}
private PlcType _selectedPlcType = PlcType.S1200;
public PlcType SelectedPlcType
{
get => _selectedPlcType;
set => SetProperty(ref _selectedPlcType, value);
}
/// <summary>
/// PLC 类型枚举值列表,用于下拉框绑定 | PLC type enum values for ComboBox binding
/// </summary>
public PlcType[] PlcTypes => Enum.GetValues(typeof(PlcType)).Cast<PlcType>().ToArray();
/// <summary>
/// 连接状态(绑定 PlcService.IsConnected| Connection status
/// </summary>
public bool IsConnected => _plcService.IsConnected;
/// <summary>
/// 状态文本(绑定 PlcService.StatusText| Status text
/// </summary>
public string StatusText => _plcService.StatusText;
private bool _isConnecting;
/// <summary>
/// 连接中标志,防止重复点击 | Connecting flag to prevent duplicate clicks
/// </summary>
public bool IsConnecting
{
get => _isConnecting;
set
{
if (SetProperty(ref _isConnecting, value))
RefreshCommandStates();
}
}
// === 单点读写区属性 | Single point read/write properties ===
private string _pointAddress;
public string PointAddress
{
get => _pointAddress;
set
{
if (SetProperty(ref _pointAddress, value))
RefreshCommandStates();
}
}
private string _selectedDataType = "Int";
public string SelectedDataType
{
get => _selectedDataType;
set
{
if (SetProperty(ref _selectedDataType, value))
{
RaisePropertyChanged(nameof(IsBoolType));
RaisePropertyChanged(nameof(IsStringType));
RefreshCommandStates();
}
}
}
private string _readResult;
public string ReadResult
{
get => _readResult;
set => SetProperty(ref _readResult, value);
}
private string _writeValue;
public string WriteValue
{
get => _writeValue;
set
{
if (SetProperty(ref _writeValue, value))
RefreshCommandStates();
}
}
/// <summary>
/// 是否为 Bool 类型(控制 Toggle 按钮可见性)| Whether data type is Bool
/// </summary>
public bool IsBoolType => SelectedDataType == "Bool";
/// <summary>
/// 是否为 String 类型(控制字符串长度输入可见性)| Whether data type is String
/// </summary>
public bool IsStringType => SelectedDataType == "String";
private int _stringLength = 10;
/// <summary>
/// 字符串读写长度(字节数)| String read/write length in bytes
/// </summary>
public int StringLength
{
get => _stringLength;
set => SetProperty(ref _stringLength, value);
}
private bool _isContinuousReading;
/// <summary>
/// 是否正在连续读取 | Whether continuous reading is active
/// </summary>
public bool IsContinuousReading
{
get => _isContinuousReading;
set
{
if (SetProperty(ref _isContinuousReading, value))
{
RaisePropertyChanged(nameof(IsNotContinuousReading));
RefreshCommandStates();
}
}
}
/// <summary>
/// 是否未在连续读取(用于按钮启用绑定)| Whether not continuous reading
/// </summary>
public bool IsNotContinuousReading => !IsContinuousReading;
// === 批量读取区属性 | Batch read properties ===
private string _batchAddress;
public string BatchAddress
{
get => _batchAddress;
set
{
if (SetProperty(ref _batchAddress, value))
RefreshCommandStates();
}
}
private string _batchLength;
public string BatchLength
{
get => _batchLength;
set
{
if (SetProperty(ref _batchLength, value))
RefreshCommandStates();
}
}
private string _hexViewerContent;
public string HexViewerContent
{
get => _hexViewerContent;
set => SetProperty(ref _hexViewerContent, value);
}
// === 日志区属性 | Log properties ===
public ObservableCollection<LogEntry> LogEntries { get; } = new ObservableCollection<LogEntry>();
// === 命令 | Commands ===
public DelegateCommand ConnectCommand { get; }
public DelegateCommand DisconnectCommand { get; }
public DelegateCommand ReadCommand { get; }
public DelegateCommand WriteCommand { get; }
public DelegateCommand ToggleBoolCommand { get; }
public DelegateCommand BatchReadCommand { get; }
public DelegateCommand ClearLogCommand { get; }
public DelegateCommand StartContinuousReadCommand { get; }
public DelegateCommand StopContinuousReadCommand { get; }
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="plcClient">PLC 客户端接口 | PLC client interface</param>
/// <param name="logger">日志服务 | Logger service</param>
public PlcTestBenchViewModel(IPlcClient plcClient, ILoggerService logger)
{
_plcClient = plcClient ?? throw new ArgumentNullException(nameof(plcClient));
_logger = logger?.ForModule<PlcTestBenchViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_plcService = new PlcService(plcClient, logger, new XmlSignalParser());
_configLoader = new ConfigLoader(logger);
_cts = new CancellationTokenSource();
// 监听 PlcService 属性变化,转发到 UI | Forward PlcService property changes to UI
_plcService.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(PlcService.IsConnected))
{
RaisePropertyChanged(nameof(IsConnected));
RefreshCommandStates();
}
else if (e.PropertyName == nameof(PlcService.StatusText))
{
RaisePropertyChanged(nameof(StatusText));
}
};
// 初始化命令 | Initialize commands
ConnectCommand = new DelegateCommand(async () => await ConnectAsync())
.ObservesCanExecute(() => CanConnect);
DisconnectCommand = new DelegateCommand(Disconnect)
.ObservesCanExecute(() => IsConnected);
ReadCommand = new DelegateCommand(async () => await ReadPointAsync(),
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress));
WriteCommand = new DelegateCommand(async () => await WritePointAsync(),
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress) && !string.IsNullOrWhiteSpace(WriteValue));
ToggleBoolCommand = new DelegateCommand(async () => await ToggleBoolAsync(),
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress) && IsBoolType);
BatchReadCommand = new DelegateCommand(async () => await BatchReadAsync(),
() => IsConnected && !string.IsNullOrWhiteSpace(BatchAddress) && !string.IsNullOrWhiteSpace(BatchLength));
ClearLogCommand = new DelegateCommand(() => LogEntries.Clear());
StartContinuousReadCommand = new DelegateCommand(StartContinuousRead,
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress) && !IsContinuousReading);
StopContinuousReadCommand = new DelegateCommand(StopContinuousRead,
() => IsContinuousReading);
// 初始化连续读取定时器(250ms 间隔)| Initialize continuous read timer (250ms interval)
_continuousReadTimer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(250)
};
_continuousReadTimer.Tick += async (s, e) => await ContinuousReadTickAsync();
// 加载配置 | Load configuration
LoadConfig();
}
/// <summary>
/// 连接按钮启用条件 | Connect button enable condition
/// </summary>
public bool CanConnect => !IsConnecting && !IsConnected;
/// <summary>
/// 加载 PLC 配置 | Load PLC configuration
/// </summary>
private void LoadConfig()
{
try
{
var config = _configLoader.LoadPlcConfig();
IpAddress = config.IpAddress;
Port = config.Port;
Rack = config.Rack;
Slot = config.Slot;
SelectedPlcType = config.PlcType;
AddLog(TestBenchLogLevel.INFO, "配置加载成功 | Configuration loaded successfully");
}
catch (Exception ex)
{
// 使用默认值 | Use default values
IpAddress = "127.0.0.1";
Port = 502;
Rack = 0;
Slot = 1;
SelectedPlcType = PlcType.S1200;
AddLog(TestBenchLogLevel.ERROR, $"配置加载失败,使用默认值 | Configuration load failed, using defaults: {ex.Message}");
}
}
/// <summary>
/// 添加日志条目 | Add log entry
/// </summary>
public void AddLog(TestBenchLogLevel level, string message)
{
var entry = new LogEntry
{
Timestamp = DateTime.Now.ToString("HH:mm:ss.fff"),
Level = level,
Message = message
};
LogEntries.Add(entry);
}
/// <summary>
/// 刷新所有命令的启用状态 | Refresh all command enable states
/// </summary>
private void RefreshCommandStates()
{
RaisePropertyChanged(nameof(CanConnect));
ReadCommand.RaiseCanExecuteChanged();
WriteCommand.RaiseCanExecuteChanged();
ToggleBoolCommand.RaiseCanExecuteChanged();
BatchReadCommand.RaiseCanExecuteChanged();
StartContinuousReadCommand.RaiseCanExecuteChanged();
StopContinuousReadCommand.RaiseCanExecuteChanged();
}
/// <summary>
/// 连接 PLC | Connect to PLC
/// </summary>
private async Task ConnectAsync()
{
IsConnecting = true;
try
{
var config = new PlcConfig
{
IpAddress = IpAddress,
Port = Port,
Rack = Rack,
Slot = Slot,
PlcType = SelectedPlcType
};
AddLog(TestBenchLogLevel.INFO, $"正在连接 PLC {IpAddress}:{Port} ... | Connecting to PLC...");
var result = await _plcService.InitializeAsync(config);
if (result)
AddLog(TestBenchLogLevel.INFO, "PLC 连接成功 | PLC connected successfully");
else
AddLog(TestBenchLogLevel.WARN, "PLC 连接失败 | PLC connection failed");
}
catch (PlcException ex)
{
AddLog(TestBenchLogLevel.ERROR, $"连接失败: {ex.Message} | Connection failed: {ex.Message}");
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"连接异常: {ex.Message} | Connection exception: {ex.Message}");
}
finally
{
IsConnecting = false;
}
}
/// <summary>
/// 断开 PLC 连接 | Disconnect PLC
/// </summary>
private void Disconnect()
{
try
{
_plcService.Dispose();
AddLog(TestBenchLogLevel.INFO, "PLC 已断开 | PLC disconnected");
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"断开失败: {ex.Message} | Disconnect failed: {ex.Message}");
}
}
/// <summary>
/// 读取单点数据 | Read single point data
/// </summary>
private async Task ReadPointAsync()
{
if (string.IsNullOrWhiteSpace(PointAddress))
{
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
return;
}
try
{
object result = SelectedDataType switch
{
"Bool" => await _plcService.ReadValueAsync<bool>(PointAddress),
"Byte" => await _plcService.ReadValueAsync<byte>(PointAddress),
"Short" => await _plcService.ReadValueAsync<short>(PointAddress),
"Int" => await _plcService.ReadValueAsync<int>(PointAddress),
"Float" => await _plcService.ReadValueAsync<float>(PointAddress),
"Double" => await _plcService.ReadValueAsync<double>(PointAddress),
"String" => await _plcService.ReadStringAsync(PointAddress, (ushort)StringLength),
_ => throw new InvalidOperationException($"不支持的数据类型: {SelectedDataType}")
};
ReadResult = result?.ToString() ?? "null";
AddLog(TestBenchLogLevel.INFO, $"读取成功: 地址={PointAddress}, 类型={SelectedDataType}, 值={ReadResult} | Read success");
}
catch (PlcException ex)
{
AddLog(TestBenchLogLevel.ERROR, $"读取失败: 地址={PointAddress}, 错误={ex.Message} | Read failed");
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"读取异常: {ex.Message} | Read exception");
}
}
/// <summary>
/// 写入单点数据 | Write single point data
/// </summary>
private async Task WritePointAsync()
{
if (string.IsNullOrWhiteSpace(PointAddress))
{
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
return;
}
if (string.IsNullOrWhiteSpace(WriteValue))
{
AddLog(TestBenchLogLevel.ERROR, "写入值不能为空 | Write value cannot be empty");
return;
}
try
{
var converted = ConvertToType(WriteValue, SelectedDataType);
if (converted == null)
return;
bool result = SelectedDataType switch
{
"Bool" => await _plcService.WriteValueAsync(PointAddress, (bool)converted),
"Byte" => await _plcService.WriteValueAsync(PointAddress, (byte)converted),
"Short" => await _plcService.WriteValueAsync(PointAddress, (short)converted),
"Int" => await _plcService.WriteValueAsync(PointAddress, (int)converted),
"Float" => await _plcService.WriteValueAsync(PointAddress, (float)converted),
"Double" => await _plcService.WriteValueAsync(PointAddress, (double)converted),
"String" => await _plcService.WriteStringAsync(PointAddress, (string)converted, (ushort)StringLength),
_ => false
};
if (result)
AddLog(TestBenchLogLevel.INFO, $"写入成功: 地址={PointAddress}, 类型={SelectedDataType}, 值={WriteValue} | Write success");
else
AddLog(TestBenchLogLevel.ERROR, $"写入失败: 地址={PointAddress} | Write failed");
}
catch (PlcException ex)
{
AddLog(TestBenchLogLevel.ERROR, $"写入失败: 地址={PointAddress}, 错误={ex.Message} | Write failed");
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"写入异常: {ex.Message} | Write exception");
}
}
/// <summary>
/// 将字符串转换为指定数据类型 | Convert string to specified data type
/// </summary>
private object ConvertToType(string value, string dataType)
{
try
{
return dataType switch
{
"Bool" => bool.Parse(value),
"Byte" => byte.Parse(value),
"Short" => short.Parse(value),
"Int" => int.Parse(value),
"Float" => float.Parse(value),
"Double" => double.Parse(value),
"String" => value,
_ => null
};
}
catch (FormatException)
{
AddLog(TestBenchLogLevel.ERROR, $"类型转换失败: 无法将 '{value}' 转换为 {dataType} | Type conversion failed");
return null;
}
catch (OverflowException)
{
AddLog(TestBenchLogLevel.ERROR, $"类型转换失败: '{value}' 超出 {dataType} 范围 | Value overflow");
return null;
}
}
/// <summary>
/// 切换 Bool 值 | Toggle bool value
/// </summary>
private async Task ToggleBoolAsync()
{
if (string.IsNullOrWhiteSpace(PointAddress))
{
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
return;
}
try
{
var currentValue = await _plcService.ReadValueAsync<bool>(PointAddress);
var newValue = !currentValue;
var result = await _plcService.WriteValueAsync(PointAddress, newValue);
if (result)
{
ReadResult = newValue.ToString();
AddLog(TestBenchLogLevel.INFO, $"切换成功: 地址={PointAddress}, {currentValue} → {newValue} | Toggle success");
}
else
{
AddLog(TestBenchLogLevel.ERROR, $"切换失败: 地址={PointAddress} | Toggle failed");
}
}
catch (PlcException ex)
{
AddLog(TestBenchLogLevel.ERROR, $"切换失败: 地址={PointAddress}, 错误={ex.Message} | Toggle failed");
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"切换异常: {ex.Message} | Toggle exception");
}
}
/// <summary>
/// 批量读取数据 | Batch read data
/// </summary>
private async Task BatchReadAsync()
{
if (string.IsNullOrWhiteSpace(BatchAddress))
{
AddLog(TestBenchLogLevel.ERROR, "起始地址不能为空 | Start address cannot be empty");
return;
}
if (!int.TryParse(BatchLength, out int length) || length <= 0)
{
AddLog(TestBenchLogLevel.ERROR, $"字节长度无效: {BatchLength} | Invalid byte length");
return;
}
var parsed = ParseBatchAddress(BatchAddress);
if (parsed == null)
{
AddLog(TestBenchLogLevel.ERROR, $"地址格式错误: {BatchAddress},正确格式为 DB{{N}}.{{StartAddress}} | Invalid address format");
return;
}
try
{
var (dbBlock, startAddress) = parsed.Value;
var data = await _plcClient.ReadBytesAsync(dbBlock, startAddress, length);
HexViewerContent = HexFormatter.Format(data, startAddress);
AddLog(TestBenchLogLevel.INFO, $"批量读取成功: {dbBlock}.{startAddress}, 长度={length} 字节 | Batch read success");
}
catch (PlcException ex)
{
AddLog(TestBenchLogLevel.ERROR, $"批量读取失败: {ex.Message} | Batch read failed");
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"批量读取异常: {ex.Message} | Batch read exception");
}
}
/// <summary>
/// 解析批量读取地址 | Parse batch read address
/// 输入格式:"DB{N}.{StartAddress}"
/// </summary>
private (string dbBlock, int startAddress)? ParseBatchAddress(string address)
{
if (string.IsNullOrWhiteSpace(address))
return null;
var dotIndex = address.IndexOf('.');
if (dotIndex <= 0 || dotIndex >= address.Length - 1)
return null;
var dbBlock = address.Substring(0, dotIndex);
var startPart = address.Substring(dotIndex + 1);
if (!dbBlock.StartsWith("DB", StringComparison.OrdinalIgnoreCase))
return null;
if (!int.TryParse(startPart, out int startAddress))
return null;
return (dbBlock, startAddress);
}
/// <summary>
/// 开始连续读取 | Start continuous reading
/// </summary>
private void StartContinuousRead()
{
if (string.IsNullOrWhiteSpace(PointAddress))
{
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
return;
}
IsContinuousReading = true;
_continuousReadTimer.Start();
AddLog(TestBenchLogLevel.INFO, $"开始连续读取: 地址={PointAddress}, 类型={SelectedDataType}, 间隔=250ms | Continuous read started");
}
/// <summary>
/// 停止连续读取 | Stop continuous reading
/// </summary>
private void StopContinuousRead()
{
_continuousReadTimer.Stop();
IsContinuousReading = false;
AddLog(TestBenchLogLevel.INFO, "连续读取已停止 | Continuous read stopped");
}
/// <summary>
/// 连续读取定时器回调 | Continuous read timer tick callback
/// </summary>
private async Task ContinuousReadTickAsync()
{
if (!IsConnected || string.IsNullOrWhiteSpace(PointAddress))
{
StopContinuousRead();
return;
}
try
{
object result = SelectedDataType switch
{
"Bool" => await _plcService.ReadValueAsync<bool>(PointAddress),
"Byte" => await _plcService.ReadValueAsync<byte>(PointAddress),
"Short" => await _plcService.ReadValueAsync<short>(PointAddress),
"Int" => await _plcService.ReadValueAsync<int>(PointAddress),
"Float" => await _plcService.ReadValueAsync<float>(PointAddress),
"Double" => await _plcService.ReadValueAsync<double>(PointAddress),
"String" => await _plcService.ReadStringAsync(PointAddress, (ushort)StringLength),
_ => throw new InvalidOperationException($"不支持的数据类型: {SelectedDataType}")
};
ReadResult = result?.ToString() ?? "null";
}
catch (Exception ex)
{
AddLog(TestBenchLogLevel.ERROR, $"连续读取异常: {ex.Message} | Continuous read exception");
StopContinuousRead();
}
}
/// <summary>
/// 释放资源 | Cleanup resources
/// 窗口关闭时调用,取消异步操作并释放 PlcService
/// </summary>
public void Cleanup()
{
try
{
StopContinuousRead();
}
catch { /* 静默处理 | Silent handling */ }
try
{
_cts?.Cancel();
}
catch { /* 静默处理 | Silent handling */ }
try
{
_plcService?.Dispose();
}
catch { /* 静默处理 | Silent handling */ }
try
{
_cts?.Dispose();
}
catch { /* 静默处理 | Silent handling */ }
}
}
}