将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。

This commit is contained in:
QI Mingxuan
2026-04-16 17:31:13 +08:00
parent 6ec4c3ddaa
commit 2bd6e566c3
581 changed files with 74600 additions and 222 deletions
@@ -0,0 +1,602 @@
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()
};
}
}
}