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 { /// /// PLC 测试台 ViewModel | PLC Test Bench ViewModel /// 提供连接管理、单点读写、批量读取和日志功能 /// 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); } /// /// PLC 类型枚举值列表,用于下拉框绑定 | PLC type enum values for ComboBox binding /// public PlcType[] PlcTypes => Enum.GetValues(typeof(PlcType)).Cast().ToArray(); /// /// 连接状态(绑定 PlcService.IsConnected)| Connection status /// public bool IsConnected => _plcService.IsConnected; /// /// 状态文本(绑定 PlcService.StatusText)| Status text /// public string StatusText => _plcService.StatusText; private bool _isConnecting; /// /// 连接中标志,防止重复点击 | Connecting flag to prevent duplicate clicks /// 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(); } } /// /// 是否为 Bool 类型(控制 Toggle 按钮可见性)| Whether data type is Bool /// public bool IsBoolType => SelectedDataType == "Bool"; /// /// 是否为 String 类型(控制字符串长度输入可见性)| Whether data type is String /// public bool IsStringType => SelectedDataType == "String"; private int _stringLength = 10; /// /// 字符串读写长度(字节数)| String read/write length in bytes /// public int StringLength { get => _stringLength; set => SetProperty(ref _stringLength, value); } private bool _isContinuousReading; /// /// 是否正在连续读取 | Whether continuous reading is active /// public bool IsContinuousReading { get => _isContinuousReading; set { if (SetProperty(ref _isContinuousReading, value)) { RaisePropertyChanged(nameof(IsNotContinuousReading)); RefreshCommandStates(); } } } /// /// 是否未在连续读取(用于按钮启用绑定)| Whether not continuous reading /// 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 LogEntries { get; } = new ObservableCollection(); // === 命令 | 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; } /// /// 构造函数 | Constructor /// /// PLC 客户端接口 | PLC client interface /// 日志服务 | Logger service public PlcTestBenchViewModel(IPlcClient plcClient, ILoggerService logger) { _plcClient = plcClient ?? throw new ArgumentNullException(nameof(plcClient)); _logger = logger?.ForModule() ?? 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(); } /// /// 连接按钮启用条件 | Connect button enable condition /// public bool CanConnect => !IsConnecting && !IsConnected; /// /// 加载 PLC 配置 | Load PLC configuration /// 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}"); } } /// /// 添加日志条目 | Add log entry /// 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); } /// /// 刷新所有命令的启用状态 | Refresh all command enable states /// private void RefreshCommandStates() { RaisePropertyChanged(nameof(CanConnect)); ReadCommand.RaiseCanExecuteChanged(); WriteCommand.RaiseCanExecuteChanged(); ToggleBoolCommand.RaiseCanExecuteChanged(); BatchReadCommand.RaiseCanExecuteChanged(); StartContinuousReadCommand.RaiseCanExecuteChanged(); StopContinuousReadCommand.RaiseCanExecuteChanged(); } /// /// 连接 PLC | Connect to PLC /// 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; } } /// /// 断开 PLC 连接 | Disconnect PLC /// private void Disconnect() { try { _plcService.Dispose(); AddLog(TestBenchLogLevel.INFO, "PLC 已断开 | PLC disconnected"); } catch (Exception ex) { AddLog(TestBenchLogLevel.ERROR, $"断开失败: {ex.Message} | Disconnect failed: {ex.Message}"); } } /// /// 读取单点数据 | Read single point data /// 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(PointAddress), "Byte" => await _plcService.ReadValueAsync(PointAddress), "Short" => await _plcService.ReadValueAsync(PointAddress), "Int" => await _plcService.ReadValueAsync(PointAddress), "Float" => await _plcService.ReadValueAsync(PointAddress), "Double" => await _plcService.ReadValueAsync(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"); } } /// /// 写入单点数据 | Write single point data /// 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"); } } /// /// 将字符串转换为指定数据类型 | Convert string to specified data type /// 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; } } /// /// 切换 Bool 值 | Toggle bool value /// private async Task ToggleBoolAsync() { if (string.IsNullOrWhiteSpace(PointAddress)) { AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty"); return; } try { var currentValue = await _plcService.ReadValueAsync(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"); } } /// /// 批量读取数据 | Batch read data /// 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"); } } /// /// 解析批量读取地址 | Parse batch read address /// 输入格式:"DB{N}.{StartAddress}" /// 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); } /// /// 开始连续读取 | Start continuous reading /// 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"); } /// /// 停止连续读取 | Stop continuous reading /// private void StopContinuousRead() { _continuousReadTimer.Stop(); IsContinuousReading = false; AddLog(TestBenchLogLevel.INFO, "连续读取已停止 | Continuous read stopped"); } /// /// 连续读取定时器回调 | Continuous read timer tick callback /// private async Task ContinuousReadTickAsync() { if (!IsConnected || string.IsNullOrWhiteSpace(PointAddress)) { StopContinuousRead(); return; } try { object result = SelectedDataType switch { "Bool" => await _plcService.ReadValueAsync(PointAddress), "Byte" => await _plcService.ReadValueAsync(PointAddress), "Short" => await _plcService.ReadValueAsync(PointAddress), "Int" => await _plcService.ReadValueAsync(PointAddress), "Float" => await _plcService.ReadValueAsync(PointAddress), "Double" => await _plcService.ReadValueAsync(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(); } } /// /// 释放资源 | Cleanup resources /// 窗口关闭时调用,取消异步操作并释放 PlcService /// 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 */ } } } }