将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()
};
}
}
}
@@ -0,0 +1,272 @@
using System;
using System.Collections.ObjectModel;
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;
namespace XP.App.ViewModels
{
/// <summary>
/// 信号数据服务 DEMO 窗口 ViewModel
/// </summary>
public class SignalDataDemoViewModel : BindableBase
{
private readonly PlcService _plcService;
private readonly ISignalDataService _signalService;
private readonly ConfigLoader _configLoader;
private readonly ILoggerService _logger;
// 日志
public ObservableCollection<string> LogEntries { get; } = new();
// 连接状态
public bool IsConnected => _plcService.IsConnected;
public string StatusText => _plcService.StatusText;
private bool _isInitialized;
public bool IsInitialized
{
get => _isInitialized;
set => SetProperty(ref _isInitialized, value);
}
// 缓存读取
private string _readSignalName = "";
public string ReadSignalName
{
get => _readSignalName;
set => SetProperty(ref _readSignalName, value);
}
private string _readResult = "";
public string ReadResult
{
get => _readResult;
set => SetProperty(ref _readResult, value);
}
// 队列写入
private string _queueSignalName = "";
public string QueueSignalName
{
get => _queueSignalName;
set => SetProperty(ref _queueSignalName, value);
}
private string _queueWriteValue = "";
public string QueueWriteValue
{
get => _queueWriteValue;
set => SetProperty(ref _queueWriteValue, value);
}
// 直接写入+回读校验
private string _directSignalName = "";
public string DirectSignalName
{
get => _directSignalName;
set => SetProperty(ref _directSignalName, value);
}
private string _directWriteValue = "";
public string DirectWriteValue
{
get => _directWriteValue;
set => SetProperty(ref _directWriteValue, value);
}
// 信号定义文件路径
private string _xmlFilePath = "PlcAddrDfn.xml";
public string XmlFilePath
{
get => _xmlFilePath;
set => SetProperty(ref _xmlFilePath, value);
}
// 命令
public DelegateCommand ConnectAndLoadCommand { get; }
public DelegateCommand CacheReadCommand { get; }
public DelegateCommand QueueWriteCommand { get; }
public DelegateCommand DirectWriteCommand { get; }
public DelegateCommand ClearLogCommand { get; }
public SignalDataDemoViewModel(
PlcService plcService,
ISignalDataService signalService,
ConfigLoader configLoader,
ILoggerService logger)
{
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
_signalService = signalService ?? throw new ArgumentNullException(nameof(signalService));
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
_logger = logger?.ForModule<SignalDataDemoViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_plcService.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(PlcService.IsConnected))
RaisePropertyChanged(nameof(IsConnected));
else if (e.PropertyName == nameof(PlcService.StatusText))
RaisePropertyChanged(nameof(StatusText));
};
ConnectAndLoadCommand = new DelegateCommand(async () => await ConnectAndLoadAsync());
CacheReadCommand = new DelegateCommand(CacheRead);
QueueWriteCommand = new DelegateCommand(QueueWrite);
DirectWriteCommand = new DelegateCommand(async () => await DirectWriteAsync());
ClearLogCommand = new DelegateCommand(() => LogEntries.Clear());
}
private void Log(string msg)
{
var text = $"[{DateTime.Now:HH:mm:ss.fff}] {msg}";
LogEntries.Add(text);
}
/// <summary>
/// 连接 PLC 并加载信号定义
/// </summary>
private async Task ConnectAndLoadAsync()
{
try
{
Log("正在加载配置...");
var config = _configLoader.LoadPlcConfig();
Log($"配置加载成功: {config.IpAddress}:{config.Port}, DB读={config.ReadDbBlock}");
Log("正在连接 PLC...");
var result = await _plcService.InitializeAsync(config);
if (!result)
{
Log("ERROR: PLC 连接失败");
return;
}
Log("PLC 连接成功");
Log($"正在加载信号定义: {XmlFilePath}");
_plcService.LoadSignalDefinitions(XmlFilePath);
Log("信号定义加载成功");
IsInitialized = true;
}
catch (Exception ex)
{
Log($"ERROR: {ex.Message}");
}
}
/// <summary>
/// 缓存读取
/// </summary>
private void CacheRead()
{
if (string.IsNullOrWhiteSpace(ReadSignalName))
{
Log("ERROR: 请输入信号名称");
return;
}
try
{
var value = _signalService.GetValueByName(ReadSignalName);
ReadResult = value?.ToString() ?? "null";
Log($"缓存读取成功: {ReadSignalName} = {ReadResult} (类型: {value?.GetType().Name ?? "null"})");
}
catch (PlcException ex)
{
ReadResult = "ERROR";
Log($"ERROR: 缓存读取失败 - {ex.Message}");
}
catch (Exception ex)
{
ReadResult = "ERROR";
Log($"ERROR: {ex.Message}");
}
}
/// <summary>
/// 队列写入
/// </summary>
private void QueueWrite()
{
if (string.IsNullOrWhiteSpace(QueueSignalName))
{
Log("ERROR: 请输入信号名称");
return;
}
if (string.IsNullOrWhiteSpace(QueueWriteValue))
{
Log("ERROR: 请输入写入值");
return;
}
try
{
// 尝试解析为数值,否则作为字符串
object value = ParseInputValue(QueueWriteValue);
bool enqueued = _signalService.EnqueueWrite(QueueSignalName, value);
Log(enqueued
? $"队列写入成功: {QueueSignalName} = {QueueWriteValue}"
: $"WARN: 队列写入返回 false(队列未运行或已满): {QueueSignalName}");
}
catch (PlcException ex)
{
Log($"ERROR: 队列写入失败 - {ex.Message}");
}
catch (Exception ex)
{
Log($"ERROR: {ex.Message}");
}
}
/// <summary>
/// 直接写入+回读校验
/// </summary>
private async Task DirectWriteAsync()
{
if (string.IsNullOrWhiteSpace(DirectSignalName))
{
Log("ERROR: 请输入信号名称");
return;
}
if (string.IsNullOrWhiteSpace(DirectWriteValue))
{
Log("ERROR: 请输入写入值");
return;
}
try
{
object value = ParseInputValue(DirectWriteValue);
Log($"正在直接写入: {DirectSignalName} = {DirectWriteValue}...");
bool verified = await _signalService.WriteDirectWithVerify(DirectSignalName, value);
Log(verified
? $"直接写入校验通过: {DirectSignalName} = {DirectWriteValue}"
: $"WARN: 直接写入校验失败: {DirectSignalName} = {DirectWriteValue}");
}
catch (PlcException ex)
{
Log($"ERROR: 直接写入失败 - {ex.Message}");
}
catch (Exception ex)
{
Log($"ERROR: {ex.Message}");
}
}
/// <summary>
/// 解析用户输入值:尝试 bool → int → double → string
/// </summary>
private object ParseInputValue(string input)
{
if (bool.TryParse(input, out bool bVal)) return bVal;
if (int.TryParse(input, out int iVal)) return iVal;
if (double.TryParse(input, out double dVal)) return dVal;
return input;
}
}
}
@@ -0,0 +1,47 @@
using System.Collections.ObjectModel;
using Prism.Mvvm;
namespace XP.Hardware.PLC.Sentry.ViewModels
{
/// <summary>
/// 信号分组 ViewModel,对应一个 Tab 页 | Signal group ViewModel, corresponds to one Tab page
/// </summary>
public class SignalGroupViewModel : BindableBase
{
private string _groupId = string.Empty;
private int _dbNumber;
private ObservableCollection<SignalRowViewModel> _signals = new();
/// <summary>
/// 分组 ID | Group ID
/// </summary>
public string GroupId
{
get => _groupId;
set => SetProperty(ref _groupId, value ?? string.Empty);
}
/// <summary>
/// DB 块号 | DB block number
/// </summary>
public int DBNumber
{
get => _dbNumber;
set => SetProperty(ref _dbNumber, value);
}
/// <summary>
/// 标签页标题,格式 "{GroupId} [DB{DBNumber}]" | Tab header, format "{GroupId} [DB{DBNumber}]"
/// </summary>
public string TabHeader => $"{GroupId} [DB{DBNumber}]";
/// <summary>
/// 该分组下的信号行集合 | Signal row collection under this group
/// </summary>
public ObservableCollection<SignalRowViewModel> Signals
{
get => _signals;
set => SetProperty(ref _signals, value ?? new ObservableCollection<SignalRowViewModel>());
}
}
}
@@ -0,0 +1,204 @@
using System;
using System.Threading.Tasks;
using Prism.Commands;
using Prism.Mvvm;
using XP.Hardware.PLC.Models;
namespace XP.Hardware.PLC.Sentry.ViewModels
{
/// <summary>
/// 单行信号 ViewModel,封装 SignalEntry 并添加运行时状态 | Single signal row ViewModel wrapping SignalEntry with runtime state
/// </summary>
public class SignalRowViewModel : BindableBase
{
private readonly SignalEntry _signal;
private string _currentValue = "--";
private bool _hasReadError;
private object _writeValue;
private string _validationError;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="signal">信号定义条目 | Signal definition entry</param>
/// <param name="applyAction">队列写入回调 | Queue write callback</param>
/// <param name="directWriteAction">直接写入回调 | Direct write callback</param>
/// <param name="canExecuteWrite">写入命令是否可用判断 | Write command can-execute predicate</param>
public SignalRowViewModel(
SignalEntry signal,
Action<SignalRowViewModel> applyAction,
Action<SignalRowViewModel> directWriteAction,
Func<bool> canExecuteWrite)
{
_signal = signal ?? throw new ArgumentNullException(nameof(signal));
ApplyCommand = new DelegateCommand(
() => applyAction?.Invoke(this),
() => canExecuteWrite?.Invoke() ?? false);
DirectWriteCommand = new DelegateCommand(
() => directWriteAction?.Invoke(this),
() => canExecuteWrite?.Invoke() ?? false);
}
// === 信号定义信息(只读)| Signal definition info (read-only) ===
/// <summary>
/// 信号名称 | Signal name
/// </summary>
public string Name => _signal.Name;
/// <summary>
/// 数据类型 | Data type
/// </summary>
public string Type => _signal.Type;
/// <summary>
/// 起始地址 | Start address
/// </summary>
public int StartAddr => _signal.StartAddr;
/// <summary>
/// 长度/索引 | Length/Index
/// </summary>
public string IndexOrLength => _signal.IndexOrLength;
/// <summary>
/// 备注 | Remark
/// </summary>
public string Remark => _signal.Remark;
/// <summary>
/// DB 块号 | DB block number
/// </summary>
public int DBNumber => _signal.DBNumber;
/// <summary>
/// 原始信号定义条目 | Original signal definition entry
/// </summary>
public SignalEntry Signal => _signal;
/// <summary>
/// 格式化的地址显示 | Formatted address display
/// bool 类型显示位地址(DB{n}.{addr}.{bit}),其他类型显示字节地址(DB{n}.{addr}
/// </summary>
public string AddressDisplay => Type?.ToLowerInvariant() == "bool"
? $"DB{DBNumber}.{StartAddr}.{IndexOrLength}"
: $"DB{DBNumber}.{StartAddr}";
// === 运行时状态 | Runtime state ===
/// <summary>
/// 当前读取值 | Current read value
/// </summary>
public string CurrentValue
{
get => _currentValue;
set => SetProperty(ref _currentValue, value ?? "--");
}
/// <summary>
/// 是否存在读取错误 | Whether read error exists
/// </summary>
public bool HasReadError
{
get => _hasReadError;
set => SetProperty(ref _hasReadError, value);
}
/// <summary>
/// 写入值 | Write value
/// </summary>
public object WriteValue
{
get => _writeValue;
set
{
if (SetProperty(ref _writeValue, value))
{
// 写入值变更时清除校验错误 | Clear validation error when write value changes
ValidationError = null;
}
}
}
/// <summary>
/// 校验错误信息 | Validation error message
/// </summary>
public string ValidationError
{
get => _validationError;
set => SetProperty(ref _validationError, value);
}
// === 命令 | Commands ===
/// <summary>
/// 队列写入命令 | Queue write command
/// </summary>
public DelegateCommand ApplyCommand { get; }
/// <summary>
/// 直接写入+回读校验命令 | Direct write with verify command
/// </summary>
public DelegateCommand DirectWriteCommand { get; }
/// <summary>
/// 刷新写入命令可用状态 | Refresh write command can-execute state
/// </summary>
public void RefreshCommandState()
{
ApplyCommand.RaiseCanExecuteChanged();
DirectWriteCommand.RaiseCanExecuteChanged();
}
/// <summary>
/// 校验写入值是否符合信号数据类型要求 | Validate write value against signal data type
/// </summary>
/// <returns>校验错误信息,null 表示通过 | Validation error message, null means passed</returns>
public string ValidateWriteValue()
{
if (WriteValue == null)
return "写入值不能为空 | Write value cannot be empty";
var strValue = WriteValue.ToString();
if (string.IsNullOrWhiteSpace(strValue))
return "写入值不能为空 | Write value cannot be empty";
switch (Type?.ToLowerInvariant())
{
case "bool":
if (!bool.TryParse(strValue, out _))
return "布尔值仅接受 True/False | Bool accepts only True/False";
break;
case "byte":
if (!byte.TryParse(strValue, out _))
return "字节值超出范围 (0-255) | Byte value out of range (0-255)";
break;
case "short":
if (!short.TryParse(strValue, out _))
return "短整型值超出范围 | Short value out of range";
break;
case "int":
if (!int.TryParse(strValue, out _))
return "整型值超出范围 | Int value out of range";
break;
case "single":
if (!float.TryParse(strValue, out _))
return "浮点数格式错误 | Float format error";
break;
case "double":
if (!double.TryParse(strValue, out _))
return "双精度浮点数格式错误 | Double format error";
break;
case "string":
// 字符串接受任意非空值 | String accepts any non-empty value
break;
default:
return $"不支持的类型: {Type} | Unsupported type: {Type}";
}
return null;
}
}
}