Files

603 lines
26 KiB
C#
Raw Permalink 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.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
{
/// <summary>
/// PLC Sentry Monitor 主窗口 ViewModel | PLC Sentry Monitor main window ViewModel
/// 负责 PLC 连接管理、信号定义加载、操作日志管理 | Manages PLC connection, signal definition loading, operation log
/// </summary>
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;
/// <summary>
/// 信号值周期性刷新定时器 | Signal value periodic refresh timer
/// </summary>
private DispatcherTimer _refreshTimer;
// === 连接状态属性 | Connection status properties ===
private bool _isConnected;
/// <summary>
/// PLC 连接状态 | PLC connection status
/// </summary>
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private string _statusText = "未连接 | Not Connected";
/// <summary>
/// 状态文本 | Status text
/// </summary>
public string StatusText
{
get => _statusText;
set => SetProperty(ref _statusText, value);
}
// === 操作日志 | Operation log ===
/// <summary>
/// 操作日志集合 | Operation log entries collection
/// </summary>
public ObservableCollection<SentryLogEntry> LogEntries { get; } = new();
// === 信号分组数据 | Signal group data ===
/// <summary>
/// 信号分组集合,供 SignalMonitorView 标签页绑定 | Signal groups for tab binding
/// </summary>
public ObservableCollection<SignalGroupViewModel> SignalGroups { get; } = new();
/// <summary>
/// 是否无信号分组数据(用于显示占位提示)| Whether no signal groups (for placeholder display)
/// </summary>
public bool HasNoGroups => SignalGroups.Count == 0;
// === 命令 | Commands ===
/// <summary>
/// 连接 PLC 命令 | Connect PLC command
/// </summary>
public DelegateCommand ConnectCommand { get; }
/// <summary>
/// 断开 PLC 命令 | Disconnect PLC command
/// </summary>
public DelegateCommand DisconnectCommand { get; }
/// <summary>
/// 打开 PLC 连接管理窗口命令 | Open PLC connection management window command
/// </summary>
public DelegateCommand OpenConfigEditorCommand { get; }
/// <summary>
/// 刷新信号定义命令 | Refresh signal definitions command
/// </summary>
public DelegateCommand RefreshSignalDefinitionsCommand { get; }
/// <summary>
/// 清除日志命令 | Clear log command
/// </summary>
public DelegateCommand ClearLogCommand { get; }
/// <summary>
/// 打开信号读写测试窗口命令 | Open signal data demo window command
/// </summary>
public DelegateCommand OpenSignalDataDemoCommand { get; }
/// <summary>
/// 构造函数 | Constructor
/// </summary>
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<SentryMainViewModel>() ?? 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;
}
/// <summary>
/// 连接 PLC | Connect to PLC
/// </summary>
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");
}
}
/// <summary>
/// 断开 PLC | Disconnect from PLC
/// </summary>
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");
}
}
/// <summary>
/// 打开 PLC 连接管理窗口 | Open PLC connection management window
/// </summary>
private void OpenConfigEditor()
{
try
{
var window = _containerProvider.Resolve<PlcAddrConfigEditorWindow>();
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");
}
}
/// <summary>
/// 打开信号读写测试窗口 | Open signal data demo window
/// </summary>
private void OpenSignalDataDemo()
{
try
{
var window = _containerProvider.Resolve<SignalDataDemoWindow>();
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");
}
}
/// <summary>
/// 刷新信号定义 | Refresh signal definitions
/// </summary>
private void RefreshSignalDefinitions()
{
LoadSignalDefinitions();
}
/// <summary>
/// 加载信号定义文件并生成分组标签页 | Load signal definition file and generate group tabs
/// </summary>
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");
}
}
/// <summary>
/// 根据 SignalGroup 列表构建 SignalGroupViewModel 集合 | Build SignalGroupViewModel collection from SignalGroup list
/// </summary>
/// <param name="groups">信号分组列表 | Signal group list</param>
private void BuildSignalGroups(System.Collections.Generic.List<SignalGroup> 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));
});
}
/// <summary>
/// 队列写入回调(由 SignalRowViewModel.ApplyCommand 触发)| Queue write callback (triggered by ApplyCommand)
/// </summary>
/// <param name="row">信号行 ViewModel | Signal row ViewModel</param>
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);
}
}
/// <summary>
/// 直接写入+回读校验回调(由 SignalRowViewModel.DirectWriteCommand 触发)| Direct write callback
/// </summary>
/// <param name="row">信号行 ViewModel | Signal row ViewModel</param>
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);
}
}
/// <summary>
/// 刷新所有信号行的写入命令可用状态 | Refresh write command state for all signal rows
/// </summary>
private void RefreshAllSignalCommandStates()
{
foreach (var group in SignalGroups)
{
foreach (var row in group.Signals)
{
row.RefreshCommandState();
}
}
}
/// <summary>
/// 获取 PlcAddrDfn.xml 文件路径 | Get PlcAddrDfn.xml file path
/// </summary>
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;
}
/// <summary>
/// 添加操作日志 | Add operation log entry
/// </summary>
/// <param name="level">日志级别 | Log level</param>
/// <param name="message">日志消息 | Log message</param>
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 ===
/// <summary>
/// 启动信号值刷新定时器 | Start signal value refresh timer
/// </summary>
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();
});
}
/// <summary>
/// 停止信号值刷新定时器 | Stop signal value refresh timer
/// </summary>
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;
});
}
/// <summary>
/// 刷新定时器回调,更新所有信号的当前值 | Refresh timer callback, update all signal current values
/// </summary>
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);
}
}
}
/// <summary>
/// 刷新单个信号的当前值(异常隔离)| Refresh single signal current value (error isolated)
/// 单个信号读取异常不影响其他信号 | Single signal read error does not affect others
/// </summary>
/// <param name="row">信号行 ViewModel | Signal row ViewModel</param>
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;
}
}
/// <summary>
/// 格式化信号值显示 | Format signal value display
/// </summary>
/// <param name="signalType">信号数据类型 | Signal data type</param>
/// <param name="value">信号值 | Signal value</param>
/// <returns>格式化后的字符串 | Formatted string</returns>
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()
};
}
}
}