将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user