using System; using System.Collections.ObjectModel; using System.Configuration; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using Prism.Commands; using Prism.Ioc; 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; using XP.Hardware.PLC.Models; using XP.Hardware.PLC.Sentry.Models; using XP.App.Views; using XP.Hardware.PLC.Views; namespace XP.Hardware.PLC.Sentry.ViewModels { /// /// PLC Sentry Monitor 主窗口 ViewModel | PLC Sentry Monitor main window ViewModel /// 负责 PLC 连接管理、信号定义加载、操作日志管理 | Manages PLC connection, signal definition loading, operation log /// public class SentryMainViewModel : BindableBase { private readonly PlcService _plcService; private readonly ISignalDataService _signalDataService; private readonly PLC.Configs.ConfigLoader _configLoader; private readonly XmlSignalParser _signalParser; private readonly ILoggerService _logger; private readonly IContainerProvider _containerProvider; /// /// 信号值周期性刷新定时器 | Signal value periodic refresh timer /// private DispatcherTimer _refreshTimer; // === 连接状态属性 | Connection status properties === private bool _isConnected; /// /// PLC 连接状态 | PLC connection status /// public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); } private string _statusText = "未连接 | Not Connected"; /// /// 状态文本 | Status text /// public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); } // === 操作日志 | Operation log === /// /// 操作日志集合 | Operation log entries collection /// public ObservableCollection LogEntries { get; } = new(); // === 信号分组数据 | Signal group data === /// /// 信号分组集合,供 SignalMonitorView 标签页绑定 | Signal groups for tab binding /// public ObservableCollection SignalGroups { get; } = new(); /// /// 是否无信号分组数据(用于显示占位提示)| Whether no signal groups (for placeholder display) /// public bool HasNoGroups => SignalGroups.Count == 0; // === 命令 | Commands === /// /// 连接 PLC 命令 | Connect PLC command /// public DelegateCommand ConnectCommand { get; } /// /// 断开 PLC 命令 | Disconnect PLC command /// public DelegateCommand DisconnectCommand { get; } /// /// 打开 PLC 连接管理窗口命令 | Open PLC connection management window command /// public DelegateCommand OpenConfigEditorCommand { get; } /// /// 刷新信号定义命令 | Refresh signal definitions command /// public DelegateCommand RefreshSignalDefinitionsCommand { get; } /// /// 清除日志命令 | Clear log command /// public DelegateCommand ClearLogCommand { get; } /// /// 打开信号读写测试窗口命令 | Open signal data demo window command /// public DelegateCommand OpenSignalDataDemoCommand { get; } /// /// 构造函数 | Constructor /// public SentryMainViewModel( PlcService plcService, ISignalDataService signalDataService, PLC.Configs.ConfigLoader configLoader, XmlSignalParser signalParser, ILoggerService logger, IContainerProvider containerProvider) { _plcService = plcService ?? throw new ArgumentNullException(nameof(plcService)); _signalDataService = signalDataService ?? throw new ArgumentNullException(nameof(signalDataService)); _configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader)); _signalParser = signalParser ?? throw new ArgumentNullException(nameof(signalParser)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _containerProvider = containerProvider ?? throw new ArgumentNullException(nameof(containerProvider)); // 监听 PlcService 属性变更,同步连接状态 | Listen to PlcService property changes, sync connection status _plcService.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(PlcService.IsConnected)) { var wasConnected = IsConnected; IsConnected = _plcService.IsConnected; ConnectCommand.RaiseCanExecuteChanged(); DisconnectCommand.RaiseCanExecuteChanged(); // 刷新所有信号行的写入命令可用状态 | Refresh write command state for all signal rows RefreshAllSignalCommandStates(); // 记录连接状态变化到日志 | Log connection state change if (!wasConnected && IsConnected) { AddLog(SentryLogLevel.Info, "PLC 连接已建立 | PLC connection established"); _logger.Info("PLC 连接已建立 | PLC connection established"); } else if (wasConnected && !IsConnected) { AddLog(SentryLogLevel.Warning, "PLC 连接已断开 | PLC connection lost"); _logger.Warn("PLC 连接已断开 | PLC connection lost"); } // 连接状态变化时管理刷新定时器 | Manage refresh timer on connection state change if (_plcService.IsConnected) StartRefreshTimer(); else StopRefreshTimer(); } else if (e.PropertyName == nameof(PlcService.StatusText)) { StatusText = _plcService.StatusText; } }; // 初始化命令 | Initialize commands ConnectCommand = new DelegateCommand(async () => await ConnectAsync(), () => !IsConnected); DisconnectCommand = new DelegateCommand(Disconnect, () => IsConnected); OpenConfigEditorCommand = new DelegateCommand(OpenConfigEditor); RefreshSignalDefinitionsCommand = new DelegateCommand(RefreshSignalDefinitions); OpenSignalDataDemoCommand = new DelegateCommand(OpenSignalDataDemo); ClearLogCommand = new DelegateCommand(() => LogEntries.Clear()); // 同步初始状态 | Sync initial status IsConnected = _plcService.IsConnected; StatusText = _plcService.StatusText; } /// /// 连接 PLC | Connect to PLC /// private async Task ConnectAsync() { try { AddLog(SentryLogLevel.Info, "正在加载 PLC 配置... | Loading PLC configuration..."); var config = _configLoader.LoadPlcConfig(); AddLog(SentryLogLevel.Info, $"PLC 配置加载成功: {config.IpAddress}:{config.Port} | PLC config loaded: {config.IpAddress}:{config.Port}"); AddLog(SentryLogLevel.Info, "正在连接 PLC... | Connecting to PLC..."); var result = await _plcService.InitializeAsync(config); if (result) { AddLog(SentryLogLevel.Info, "PLC 连接成功 | PLC connected successfully"); _logger.Info("PLC 连接成功 | PLC connected successfully"); // 连接成功后自动加载信号定义 | Auto-load signal definitions after connection LoadSignalDefinitions(); } else { AddLog(SentryLogLevel.Error, "PLC 连接失败 | PLC connection failed"); _logger.Warn("PLC 连接失败 | PLC connection failed"); } } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"PLC 连接异常: {ex.Message} | PLC connection error: {ex.Message}"); _logger.Error(ex, "PLC 连接异常 | PLC connection error"); } } /// /// 断开 PLC | Disconnect from PLC /// private void Disconnect() { try { StopRefreshTimer(); _plcService.Dispose(); AddLog(SentryLogLevel.Info, "PLC 已断开 | PLC disconnected"); _logger.Info("PLC 已断开 | PLC disconnected"); } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"PLC 断开异常: {ex.Message} | PLC disconnect error: {ex.Message}"); _logger.Error(ex, "PLC 断开异常 | PLC disconnect error"); } } /// /// 打开 PLC 连接管理窗口 | Open PLC connection management window /// private void OpenConfigEditor() { try { var window = _containerProvider.Resolve(); window.Owner = Application.Current.MainWindow; window.ShowDialog(); } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"打开配置编辑器失败: {ex.Message} | Failed to open config editor: {ex.Message}"); _logger.Error(ex, "打开配置编辑器失败 | Failed to open config editor"); } } /// /// 打开信号读写测试窗口 | Open signal data demo window /// private void OpenSignalDataDemo() { try { var window = _containerProvider.Resolve(); window.Owner = Application.Current.MainWindow; window.Show(); } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"打开信号读写测试窗口失败: {ex.Message} | Failed to open signal data demo window: {ex.Message}"); _logger.Error(ex, "打开信号读写测试窗口失败 | Failed to open signal data demo window"); } } /// /// 刷新信号定义 | Refresh signal definitions /// private void RefreshSignalDefinitions() { LoadSignalDefinitions(); } /// /// 加载信号定义文件并生成分组标签页 | Load signal definition file and generate group tabs /// private void LoadSignalDefinitions() { try { var xmlPath = GetPlcAddrDfnPath(); AddLog(SentryLogLevel.Info, $"正在加载信号定义: {xmlPath} | Loading signal definitions: {xmlPath}"); // 预检查文件是否存在,提供更友好的错误提示 | Pre-check file existence for better error message if (!File.Exists(xmlPath)) { var fullPath = Path.GetFullPath(xmlPath); AddLog(SentryLogLevel.Error, $"信号定义文件不存在: {fullPath} | Signal definition file not found: {fullPath}"); AddLog(SentryLogLevel.Warning, "请检查 App.config 中 Sentry:PlcAddrDfnPath 配置,或将 PlcAddrDfn.xml 放置到应用程序目录 | Please check Sentry:PlcAddrDfnPath in App.config, or place PlcAddrDfn.xml in the application directory"); _logger.Error(new FileNotFoundException(fullPath), "信号定义文件不存在: {XmlPath} | Signal definition file not found: {XmlPath}", fullPath); return; } // 通过 PlcService 加载信号定义(注册到内部字典)| Load signal definitions via PlcService _plcService.LoadSignalDefinitions(xmlPath); // 通过 XmlSignalParser 加载分组结构用于 UI 展示 | Load group structure via XmlSignalParser for UI display var groups = _signalParser.LoadFromFile(xmlPath); BuildSignalGroups(groups); AddLog(SentryLogLevel.Info, $"信号定义加载成功,共 {groups.Count} 个分组 | Signal definitions loaded, {groups.Count} groups"); _logger.Info("信号定义加载成功: {XmlPath}, 分组数: {GroupCount} | Signal definitions loaded: {XmlPath}, groups: {GroupCount}", xmlPath, groups.Count); } catch (FileNotFoundException ex) { var filePath = ex.FileName ?? "未知路径 | unknown path"; AddLog(SentryLogLevel.Error, $"信号定义文件不存在: {filePath} | Signal definition file not found: {filePath}"); AddLog(SentryLogLevel.Warning, "请检查文件路径是否正确,确保 PlcAddrDfn.xml 文件存在 | Please verify the file path and ensure PlcAddrDfn.xml exists"); _logger.Error(ex, "信号定义文件不存在: {FilePath} | Signal definition file not found: {FilePath}", filePath); } catch (System.Xml.XmlException ex) { AddLog(SentryLogLevel.Error, $"信号定义 XML 格式错误(行 {ex.LineNumber}, 列 {ex.LinePosition}): {ex.Message} | Signal definition XML format error (line {ex.LineNumber}, col {ex.LinePosition}): {ex.Message}"); AddLog(SentryLogLevel.Warning, "请检查 PlcAddrDfn.xml 文件的 XML 格式是否正确 | Please check the XML format of PlcAddrDfn.xml"); _logger.Error(ex, "信号定义 XML 格式错误: 行={LineNumber}, 列={LinePosition} | Signal definition XML format error: line={LineNumber}, col={LinePosition}", ex.LineNumber, ex.LinePosition); } catch (PlcException ex) { AddLog(SentryLogLevel.Error, $"信号定义加载异常: {ex.Message} | Signal definition loading error: {ex.Message}"); AddLog(SentryLogLevel.Warning, "请检查 PlcAddrDfn.xml 中是否存在重复信号名称或无效配置 | Please check for duplicate signal names or invalid configuration in PlcAddrDfn.xml"); _logger.Error(ex, "信号定义加载 PLC 异常 | Signal definition loading PLC error"); } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"信号定义加载失败: {ex.Message} | Signal definition loading failed: {ex.Message}"); _logger.Error(ex, "信号定义加载失败 | Signal definition loading failed"); } } /// /// 根据 SignalGroup 列表构建 SignalGroupViewModel 集合 | Build SignalGroupViewModel collection from SignalGroup list /// /// 信号分组列表 | Signal group list private void BuildSignalGroups(System.Collections.Generic.List groups) { Application.Current?.Dispatcher?.Invoke(() => { SignalGroups.Clear(); foreach (var group in groups) { var groupVm = new SignalGroupViewModel { GroupId = group.GroupId, DBNumber = group.DBNumber }; foreach (var signal in group.Signals) { var rowVm = new SignalRowViewModel( signal, applyAction: OnApplyWrite, directWriteAction: OnDirectWrite, canExecuteWrite: () => IsConnected); groupVm.Signals.Add(rowVm); } SignalGroups.Add(groupVm); } RaisePropertyChanged(nameof(HasNoGroups)); }); } /// /// 队列写入回调(由 SignalRowViewModel.ApplyCommand 触发)| Queue write callback (triggered by ApplyCommand) /// /// 信号行 ViewModel | Signal row ViewModel private void OnApplyWrite(SignalRowViewModel row) { try { // 类型校验(委托给 SignalRowViewModel)| Type validation (delegated to SignalRowViewModel) var validationError = row.ValidateWriteValue(); if (validationError != null) { row.ValidationError = validationError; return; } var result = _signalDataService.EnqueueWrite(row.Name, row.WriteValue); if (result) { AddLog(SentryLogLevel.Info, $"写入入队: {row.Name} = {row.WriteValue} | Write enqueued: {row.Name} = {row.WriteValue}"); _logger.Info("写入入队: 信号={SignalName}, 值={WriteValue} | Write enqueued: signal={SignalName}, value={WriteValue}", row.Name, row.WriteValue); } else { AddLog(SentryLogLevel.Warning, $"写入入队失败(队列未运行或已满): {row.Name} | Write enqueue failed (queue not running or full): {row.Name}"); _logger.Warn("写入入队失败: 信号={SignalName} | Write enqueue failed: signal={SignalName}", row.Name); } } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"写入异常: {row.Name}, {ex.Message} | Write error: {row.Name}, {ex.Message}"); _logger.Error(ex, "队列写入异常: 信号={SignalName} | Queue write error: signal={SignalName}", row.Name); } } /// /// 直接写入+回读校验回调(由 SignalRowViewModel.DirectWriteCommand 触发)| Direct write callback /// /// 信号行 ViewModel | Signal row ViewModel private async void OnDirectWrite(SignalRowViewModel row) { try { // 类型校验(委托给 SignalRowViewModel)| Type validation (delegated to SignalRowViewModel) var validationError = row.ValidateWriteValue(); if (validationError != null) { row.ValidationError = validationError; return; } var result = await _signalDataService.WriteDirectWithVerify(row.Name, row.WriteValue); if (result) { AddLog(SentryLogLevel.Info, $"直接写入成功: {row.Name} = {row.WriteValue} | Direct write success: {row.Name} = {row.WriteValue}"); _logger.Info("直接写入成功: 信号={SignalName}, 值={WriteValue} | Direct write success: signal={SignalName}, value={WriteValue}", row.Name, row.WriteValue); } else { AddLog(SentryLogLevel.Warning, $"直接写入回读校验失败: {row.Name} | Direct write verification failed: {row.Name}"); _logger.Warn("直接写入回读校验失败: 信号={SignalName} | Direct write verification failed: signal={SignalName}", row.Name); } } catch (Exception ex) { AddLog(SentryLogLevel.Error, $"直接写入异常: {row.Name}, {ex.Message} | Direct write error: {row.Name}, {ex.Message}"); _logger.Error(ex, "直接写入异常: 信号={SignalName} | Direct write error: signal={SignalName}", row.Name); } } /// /// 刷新所有信号行的写入命令可用状态 | Refresh write command state for all signal rows /// private void RefreshAllSignalCommandStates() { foreach (var group in SignalGroups) { foreach (var row in group.Signals) { row.RefreshCommandState(); } } } /// /// 获取 PlcAddrDfn.xml 文件路径 | Get PlcAddrDfn.xml file path /// private string GetPlcAddrDfnPath() { var configPath = ConfigurationManager.AppSettings["Sentry:PlcAddrDfnPath"]; if (string.IsNullOrEmpty(configPath)) configPath = "PlcAddrDfn.xml"; // 如果是相对路径,则基于应用程序目录 | If relative path, base on app directory if (!Path.IsPathRooted(configPath)) configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configPath); return configPath; } /// /// 添加操作日志 | Add operation log entry /// /// 日志级别 | Log level /// 日志消息 | Log message public void AddLog(SentryLogLevel level, string message) { var entry = new SentryLogEntry { Timestamp = DateTime.Now, Level = level, Message = message }; // 插入到集合头部,实现时间倒序 | Insert at beginning for reverse chronological order Application.Current?.Dispatcher?.Invoke(() => { LogEntries.Insert(0, entry); }); } // === 信号值周期性刷新 | Signal value periodic refresh === /// /// 启动信号值刷新定时器 | Start signal value refresh timer /// private void StartRefreshTimer() { if (_refreshTimer != null) return; // 确保在 UI 线程上创建 DispatcherTimer | Ensure DispatcherTimer is created on UI thread Application.Current?.Dispatcher?.Invoke(() => { if (_refreshTimer != null) return; _refreshTimer = new DispatcherTimer { // 刷新周期 250ms,与批量读取周期一致 | Refresh interval 250ms, same as bulk read cycle Interval = TimeSpan.FromMilliseconds(250) }; _refreshTimer.Tick += RefreshTimer_Tick; _refreshTimer.Start(); }); } /// /// 停止信号值刷新定时器 | Stop signal value refresh timer /// private void StopRefreshTimer() { if (_refreshTimer == null) return; // 确保在 UI 线程上停止 DispatcherTimer | Ensure DispatcherTimer is stopped on UI thread Application.Current?.Dispatcher?.Invoke(() => { if (_refreshTimer == null) return; _refreshTimer.Stop(); _refreshTimer.Tick -= RefreshTimer_Tick; _refreshTimer = null; }); } /// /// 刷新定时器回调,更新所有信号的当前值 | Refresh timer callback, update all signal current values /// private void RefreshTimer_Tick(object sender, EventArgs e) { if (!IsConnected || SignalGroups.Count == 0) return; foreach (var group in SignalGroups) { foreach (var row in group.Signals) { RefreshSignalValue(row); } } } /// /// 刷新单个信号的当前值(异常隔离)| Refresh single signal current value (error isolated) /// 单个信号读取异常不影响其他信号 | Single signal read error does not affect others /// /// 信号行 ViewModel | Signal row ViewModel private void RefreshSignalValue(SignalRowViewModel row) { try { var value = _signalDataService.GetValueByName(row.Name); row.CurrentValue = FormatSignalValue(row.Type, value); row.HasReadError = false; } catch (Exception ex) { // 异常隔离:仅标记当前信号为读取错误,不影响其他信号 | Error isolation: only mark current signal as read error if (!row.HasReadError) { // 仅在首次出错时记录日志,避免高频刷新时日志刷屏 | Only log on first error to avoid log flooding AddLog(SentryLogLevel.Error, $"信号读取异常: {row.Name}, {ex.Message} | Signal read error: {row.Name}, {ex.Message}"); _logger.Error(ex, "信号读取异常: 信号={SignalName} | Signal read error: signal={SignalName}", row.Name); } row.CurrentValue = "读取错误"; row.HasReadError = true; } } /// /// 格式化信号值显示 | Format signal value display /// /// 信号数据类型 | Signal data type /// 信号值 | Signal value /// 格式化后的字符串 | Formatted string private static string FormatSignalValue(string signalType, object value) { if (value == null) return "--"; return signalType?.ToLowerInvariant() switch { "bool" => value is bool b ? (b ? "True" : "False") : value.ToString(), _ => value.ToString() }; } } }