将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
namespace XP.Hardware.PLC.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试台日志级别枚举 | Test bench log level enumeration
|
||||
/// </summary>
|
||||
public enum TestBenchLogLevel
|
||||
{
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试台日志条目模型 | Test bench log entry model
|
||||
/// </summary>
|
||||
public class LogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间戳,格式 "HH:mm:ss.fff" | Timestamp in "HH:mm:ss.fff" format
|
||||
/// </summary>
|
||||
public string Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志级别 | Log level
|
||||
/// </summary>
|
||||
public TestBenchLogLevel Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 日志消息 | Log message
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用于显示的格式化文本 | Formatted text for display
|
||||
/// </summary>
|
||||
public string DisplayText => $"[{Timestamp}] [{Level}] {Message}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.Localization;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.PLC.Configs;
|
||||
using XP.Hardware.PLC.Helpers;
|
||||
using XP.Hardware.PLC.Models;
|
||||
|
||||
namespace XP.Hardware.PLC.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 信号地址定义编辑器 ViewModel | PLC signal address config editor ViewModel
|
||||
/// 提供信号表加载/编辑/保存和 PLC 连接参数配置功能
|
||||
/// </summary>
|
||||
public class PlcAddrConfigEditorViewModel : BindableBase
|
||||
{
|
||||
// === 依赖 | Dependencies ===
|
||||
private readonly XmlSignalParser _xmlParser;
|
||||
private readonly ConfigLoader _configLoader;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
// === 信号分组数据 | Signal group data ===
|
||||
|
||||
/// <summary>
|
||||
/// 信号分组集合,每个 Group 对应一个 Tab 页 | Signal group collection, each Group maps to a Tab
|
||||
/// </summary>
|
||||
public ObservableCollection<SignalGroup> Groups { get; }
|
||||
|
||||
private SignalGroup _selectedGroup;
|
||||
/// <summary>
|
||||
/// 当前选中的 Group(Tab 页)| Currently selected Group (Tab)
|
||||
/// </summary>
|
||||
public SignalGroup SelectedGroup
|
||||
{
|
||||
get => _selectedGroup;
|
||||
set => SetProperty(ref _selectedGroup, value);
|
||||
}
|
||||
|
||||
// === PLC 连接参数 | PLC connection parameters ===
|
||||
|
||||
private string _ipAddress;
|
||||
/// <summary>
|
||||
/// PLC IP 地址 | PLC IP address
|
||||
/// </summary>
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set { if (SetProperty(ref _ipAddress, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _port;
|
||||
/// <summary>
|
||||
/// PLC 端口号 | PLC port number
|
||||
/// </summary>
|
||||
public int Port
|
||||
{
|
||||
get => _port;
|
||||
set { if (SetProperty(ref _port, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private short _rack;
|
||||
/// <summary>
|
||||
/// 机架号 | Rack number
|
||||
/// </summary>
|
||||
public short Rack
|
||||
{
|
||||
get => _rack;
|
||||
set { if (SetProperty(ref _rack, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private short _slot;
|
||||
/// <summary>
|
||||
/// 插槽号 | Slot number
|
||||
/// </summary>
|
||||
public short Slot
|
||||
{
|
||||
get => _slot;
|
||||
set { if (SetProperty(ref _slot, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private PlcType _selectedPlcType;
|
||||
/// <summary>
|
||||
/// 选中的 PLC 型号 | Selected PLC type
|
||||
/// </summary>
|
||||
public PlcType SelectedPlcType
|
||||
{
|
||||
get => _selectedPlcType;
|
||||
set { if (SetProperty(ref _selectedPlcType, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLC 类型枚举值列表,供下拉框绑定 | PLC type enum values for ComboBox binding
|
||||
/// </summary>
|
||||
public PlcType[] PlcTypes { get; } = Enum.GetValues(typeof(PlcType)).Cast<PlcType>().ToArray();
|
||||
|
||||
private string _readDbBlock;
|
||||
/// <summary>
|
||||
/// 读取数据块 | Read data block
|
||||
/// </summary>
|
||||
public string ReadDbBlock
|
||||
{
|
||||
get => _readDbBlock;
|
||||
set { if (SetProperty(ref _readDbBlock, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _readStartAddress;
|
||||
/// <summary>
|
||||
/// 读取起始地址 | Read start address
|
||||
/// </summary>
|
||||
public int ReadStartAddress
|
||||
{
|
||||
get => _readStartAddress;
|
||||
set { if (SetProperty(ref _readStartAddress, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _readLength;
|
||||
/// <summary>
|
||||
/// 读取长度 | Read length
|
||||
/// </summary>
|
||||
public int ReadLength
|
||||
{
|
||||
get => _readLength;
|
||||
set { if (SetProperty(ref _readLength, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _bulkReadIntervalMs = 100;
|
||||
/// <summary>
|
||||
/// 批量读取周期(毫秒)| Bulk read interval (ms)
|
||||
/// </summary>
|
||||
public int BulkReadIntervalMs
|
||||
{
|
||||
get => _bulkReadIntervalMs;
|
||||
set { if (SetProperty(ref _bulkReadIntervalMs, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _connectTimeoutMs;
|
||||
/// <summary>
|
||||
/// 连接超时(毫秒)| Connect timeout (ms)
|
||||
/// </summary>
|
||||
public int ConnectTimeoutMs
|
||||
{
|
||||
get => _connectTimeoutMs;
|
||||
set { if (SetProperty(ref _connectTimeoutMs, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _readTimeoutMs;
|
||||
/// <summary>
|
||||
/// 读取超时(毫秒)| Read timeout (ms)
|
||||
/// </summary>
|
||||
public int ReadTimeoutMs
|
||||
{
|
||||
get => _readTimeoutMs;
|
||||
set { if (SetProperty(ref _readTimeoutMs, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private int _writeTimeoutMs;
|
||||
/// <summary>
|
||||
/// 写入超时(毫秒)| Write timeout (ms)
|
||||
/// </summary>
|
||||
public int WriteTimeoutMs
|
||||
{
|
||||
get => _writeTimeoutMs;
|
||||
set { if (SetProperty(ref _writeTimeoutMs, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
private bool _bReConnect;
|
||||
/// <summary>
|
||||
/// 断线重连 | Auto reconnect on disconnect
|
||||
/// </summary>
|
||||
public bool BReConnect
|
||||
{
|
||||
get => _bReConnect;
|
||||
set { if (SetProperty(ref _bReConnect, value)) HasUnsavedChanges = true; }
|
||||
}
|
||||
|
||||
// === 文件路径 | File path ===
|
||||
|
||||
private string _xmlFilePath;
|
||||
/// <summary>
|
||||
/// XML 配置文件路径 | XML config file path
|
||||
/// </summary>
|
||||
public string XmlFilePath
|
||||
{
|
||||
get => _xmlFilePath;
|
||||
set => SetProperty(ref _xmlFilePath, value);
|
||||
}
|
||||
|
||||
// === 状态标志 | Status flags ===
|
||||
|
||||
private bool _hasUnsavedChanges;
|
||||
/// <summary>
|
||||
/// 是否有未保存的修改 | Whether there are unsaved changes
|
||||
/// </summary>
|
||||
public bool HasUnsavedChanges
|
||||
{
|
||||
get => _hasUnsavedChanges;
|
||||
set => SetProperty(ref _hasUnsavedChanges, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭回调 | Window close action callback
|
||||
/// </summary>
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支持的数据类型列表,供下拉框绑定 | Supported data types for ComboBox binding
|
||||
/// </summary>
|
||||
public string[] SupportedDataTypes { get; } = SignalEntry.SupportedTypes;
|
||||
|
||||
// === 命令 | Commands ===
|
||||
|
||||
/// <summary>保存命令 | Save command</summary>
|
||||
public DelegateCommand SaveCommand { get; }
|
||||
|
||||
/// <summary>取消命令 | Cancel command</summary>
|
||||
public DelegateCommand CancelCommand { get; }
|
||||
|
||||
/// <summary>重新加载命令 | Reload command</summary>
|
||||
public DelegateCommand ReloadCommand { get; }
|
||||
|
||||
/// <summary>添加信号命令(操作当前选中 Group)| Add signal command (operates on selected Group)</summary>
|
||||
public DelegateCommand AddSignalCommand { get; }
|
||||
|
||||
/// <summary>删除信号命令(操作当前选中 Group)| Delete signal command (operates on selected Group)</summary>
|
||||
public DelegateCommand DeleteSignalCommand { get; }
|
||||
|
||||
private SignalEntry _selectedSignal;
|
||||
/// <summary>
|
||||
/// 当前选中的信号 | Currently selected signal
|
||||
/// </summary>
|
||||
public SignalEntry SelectedSignal
|
||||
{
|
||||
get => _selectedSignal;
|
||||
set => SetProperty(ref _selectedSignal, value);
|
||||
}
|
||||
|
||||
/// <summary>新增 Group 命令 | Add Group command</summary>
|
||||
public DelegateCommand AddGroupCommand { get; }
|
||||
|
||||
/// <summary>删除 Group 命令 | Delete Group command</summary>
|
||||
public DelegateCommand DeleteGroupCommand { get; }
|
||||
|
||||
/// <summary>重命名 Group 命令 | Rename Group command</summary>
|
||||
public DelegateCommand RenameGroupCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="xmlParser">XML 信号解析器 | XML signal parser</param>
|
||||
/// <param name="configLoader">配置加载器 | Config loader</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public PlcAddrConfigEditorViewModel(
|
||||
XmlSignalParser xmlParser,
|
||||
ConfigLoader configLoader,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_xmlParser = xmlParser ?? throw new ArgumentNullException(nameof(xmlParser));
|
||||
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
|
||||
_logger = logger?.ForModule<PlcAddrConfigEditorViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// 初始化信号分组集合 | Initialize signal group collection
|
||||
Groups = new ObservableCollection<SignalGroup>();
|
||||
|
||||
// 加载 PLC 连接参数 | Load PLC connection parameters
|
||||
LoadPlcConfig();
|
||||
|
||||
// 读取 XML 文件路径,未配置时使用默认路径 | Read XML file path, fallback to default
|
||||
var configuredPath = ConfigurationManager.AppSettings["PlcAddrDfnXmlPath"];
|
||||
XmlFilePath = !string.IsNullOrWhiteSpace(configuredPath)
|
||||
? configuredPath
|
||||
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PlcAddrDfn.xml");
|
||||
|
||||
// 初始化命令 | Initialize commands
|
||||
SaveCommand = new DelegateCommand(ExecuteSave);
|
||||
CancelCommand = new DelegateCommand(ExecuteCancel);
|
||||
ReloadCommand = new DelegateCommand(ExecuteReload);
|
||||
|
||||
// 添加信号到当前选中 Group | Add signal to currently selected Group
|
||||
AddSignalCommand = new DelegateCommand(() =>
|
||||
{
|
||||
if (SelectedGroup == null)
|
||||
{
|
||||
MessageBox.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_NoGroupSelected"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_Title"),
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
var signal = new SignalEntry { OwnerCollection = SelectedGroup.Signals };
|
||||
SelectedGroup.Signals.Add(signal);
|
||||
HasUnsavedChanges = true;
|
||||
});
|
||||
|
||||
// 从当前选中 Group 删除信号 | Delete signal from currently selected Group
|
||||
DeleteSignalCommand = new DelegateCommand(() =>
|
||||
{
|
||||
if (SelectedSignal == null) return;
|
||||
if (MessageBox.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_DeleteConfirm"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_Title"),
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
||||
{
|
||||
SelectedSignal.OwnerCollection?.Remove(SelectedSignal);
|
||||
SelectedSignal = null;
|
||||
HasUnsavedChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 新增 Group | Add new Group
|
||||
AddGroupCommand = new DelegateCommand(ExecuteAddGroup);
|
||||
|
||||
// 删除当前选中 Group | Delete currently selected Group
|
||||
DeleteGroupCommand = new DelegateCommand(ExecuteDeleteGroup);
|
||||
|
||||
// 重命名当前选中 Group | Rename currently selected Group
|
||||
RenameGroupCommand = new DelegateCommand(ExecuteRenameGroup);
|
||||
|
||||
// 加载信号数据 | Load signal data
|
||||
LoadSignals();
|
||||
|
||||
_logger.Info("PLC 信号地址定义编辑器 ViewModel 初始化完成 | PlcAddrConfigEditorViewModel initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 ConfigLoader 加载 PLC 连接参数并填充绑定属性 | Load PLC config and populate binding properties
|
||||
/// </summary>
|
||||
private void LoadPlcConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = _configLoader.LoadPlcConfig();
|
||||
_ipAddress = config.IpAddress;
|
||||
_port = config.Port;
|
||||
_rack = config.Rack;
|
||||
_slot = config.Slot;
|
||||
_selectedPlcType = config.PlcType;
|
||||
_readDbBlock = config.ReadDbBlock;
|
||||
_readStartAddress = config.ReadStartAddress;
|
||||
_readLength = config.ReadLength;
|
||||
_bulkReadIntervalMs = config.BulkReadIntervalMs;
|
||||
_connectTimeoutMs = config.ConnectTimeoutMs;
|
||||
_readTimeoutMs = config.ReadTimeoutMs;
|
||||
_writeTimeoutMs = config.WriteTimeoutMs;
|
||||
_bReConnect = config.bReConnect;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn($"加载 PLC 配置失败,使用默认值 | Failed to load PLC config, using defaults: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载信号分组数据 | Load signal group data
|
||||
/// 调用 XmlSignalParser.LoadFromFile 获取 List<SignalGroup>,填充 Groups 集合
|
||||
/// </summary>
|
||||
private void LoadSignals()
|
||||
{
|
||||
try
|
||||
{
|
||||
var groups = _xmlParser.LoadFromFile(XmlFilePath);
|
||||
|
||||
Groups.Clear();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
// 为每个 Signal 设置 OwnerCollection | Set OwnerCollection for each Signal
|
||||
foreach (var signal in group.Signals)
|
||||
{
|
||||
signal.OwnerCollection = group.Signals;
|
||||
}
|
||||
Groups.Add(group);
|
||||
}
|
||||
|
||||
// 默认选中第一个 Group | Select first Group by default
|
||||
SelectedGroup = Groups.FirstOrDefault();
|
||||
|
||||
HasUnsavedChanges = false;
|
||||
var totalSignals = groups.Sum(g => g.Signals.Count);
|
||||
_logger.Info($"成功加载信号数据:共 {totalSignals} 条信号,来自 {groups.Count} 个 Group | Loaded {totalSignals} signals from {groups.Count} groups");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"加载信号数据失败 | Failed to load signals: {ex.Message}");
|
||||
Groups.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行新增 Group 操作 | Execute add Group operation
|
||||
/// 弹出输入框获取 Group ID 和 DBNumber,检查 ID 唯一性
|
||||
/// </summary>
|
||||
private void ExecuteAddGroup()
|
||||
{
|
||||
// 弹出输入框获取 Group ID,内置唯一性验证 | Prompt for Group ID with uniqueness validation
|
||||
var groupId = InputDialog.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_InputGroupId"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_AddGroup"),
|
||||
validate: input =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return LocalizationHelper.Get("Message_InvalidInput");
|
||||
if (Groups.Any(g => string.Equals(g.GroupId, input, StringComparison.Ordinal)))
|
||||
return LocalizationHelper.Get("PlcAddrEditor_GroupIdDuplicate");
|
||||
return null;
|
||||
});
|
||||
|
||||
if (groupId == null)
|
||||
return;
|
||||
|
||||
// 弹出输入框获取 DBNumber,内置非负整数验证 | Prompt for DBNumber with non-negative integer validation
|
||||
var dbNumberStr = InputDialog.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_InputDbNumber"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_AddGroup"),
|
||||
"0",
|
||||
validate: input =>
|
||||
{
|
||||
if (!int.TryParse(input, out int val) || val < 0)
|
||||
return LocalizationHelper.Get("PlcAddrEditor_InvalidDbNumber");
|
||||
return null;
|
||||
});
|
||||
|
||||
if (dbNumberStr == null)
|
||||
return;
|
||||
|
||||
var dbNumber = int.Parse(dbNumberStr);
|
||||
|
||||
// 创建新 Group 并添加到集合 | Create new Group and add to collection
|
||||
var newGroup = new SignalGroup
|
||||
{
|
||||
GroupId = groupId,
|
||||
DBNumber = dbNumber,
|
||||
Signals = new ObservableCollection<SignalEntry>()
|
||||
};
|
||||
Groups.Add(newGroup);
|
||||
SelectedGroup = newGroup;
|
||||
HasUnsavedChanges = true;
|
||||
|
||||
_logger.Info($"新增 Group: ID={groupId}, DBNumber={dbNumber} | Added Group: ID={groupId}, DBNumber={dbNumber}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行重命名 Group 操作 | Execute rename Group operation
|
||||
/// 弹出输入框获取新名称,检查唯一性
|
||||
/// </summary>
|
||||
private void ExecuteRenameGroup()
|
||||
{
|
||||
if (SelectedGroup == null)
|
||||
{
|
||||
MessageBox.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_NoGroupSelected"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_Title"),
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var oldId = SelectedGroup.GroupId;
|
||||
var newId = InputDialog.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_InputNewGroupId"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_RenameGroup"),
|
||||
oldId,
|
||||
validate: input =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return LocalizationHelper.Get("Message_InvalidInput");
|
||||
if (input == oldId)
|
||||
return null; // 没改名,允许通过 | Same name, allow
|
||||
if (Groups.Any(g => string.Equals(g.GroupId, input, StringComparison.Ordinal)))
|
||||
return LocalizationHelper.Get("PlcAddrEditor_GroupIdDuplicate");
|
||||
return null;
|
||||
});
|
||||
|
||||
if (newId == null || newId == oldId)
|
||||
return;
|
||||
|
||||
SelectedGroup.GroupId = newId;
|
||||
HasUnsavedChanges = true;
|
||||
_logger.Info($"重命名 Group: {oldId} → {newId} | Renamed Group: {oldId} → {newId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行删除 Group 操作 | Execute delete Group operation
|
||||
/// 删除前确认提示
|
||||
/// </summary>
|
||||
private void ExecuteDeleteGroup()
|
||||
{
|
||||
if (SelectedGroup == null)
|
||||
{
|
||||
MessageBox.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_NoGroupSelected"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_Title"),
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show(
|
||||
string.Format(LocalizationHelper.Get("PlcAddrEditor_DeleteGroupConfirm"), SelectedGroup.GroupId),
|
||||
LocalizationHelper.Get("PlcAddrEditor_Title"),
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
|
||||
if (result != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
var groupId = SelectedGroup.GroupId;
|
||||
Groups.Remove(SelectedGroup);
|
||||
SelectedGroup = Groups.FirstOrDefault();
|
||||
HasUnsavedChanges = true;
|
||||
|
||||
_logger.Info($"删除 Group: ID={groupId} | Deleted Group: ID={groupId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行保存操作 | Execute save operation
|
||||
/// 验证所有 Group 的 ID 唯一性、DBNumber 有效性、信号名称全局唯一性,然后保存
|
||||
/// </summary>
|
||||
private void ExecuteSave()
|
||||
{
|
||||
try
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// 1. 验证所有 Group 的 ID 唯一性 | Validate Group ID uniqueness
|
||||
var duplicateGroupIds = Groups
|
||||
.GroupBy(g => g.GroupId)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
foreach (var dupId in duplicateGroupIds)
|
||||
{
|
||||
errors.Add($"Group ID 重复 | Duplicate Group ID: {dupId}");
|
||||
}
|
||||
|
||||
// 2. 验证所有 Group 的 DBNumber 有效性(非负整数)| Validate DBNumber validity (non-negative integer)
|
||||
foreach (var group in Groups)
|
||||
{
|
||||
if (group.DBNumber < 0)
|
||||
{
|
||||
errors.Add($"Group [{group.GroupId}] 的 DBNumber 无效(必须为非负整数)| Group [{group.GroupId}] has invalid DBNumber (must be non-negative)");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 验证每个 Group 内信号名称唯一性 | Validate signal name uniqueness within each Group
|
||||
foreach (var group in Groups)
|
||||
{
|
||||
var intraGroupDuplicates = group.Signals
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.Name))
|
||||
.GroupBy(s => s.Name)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
foreach (var dupName in intraGroupDuplicates)
|
||||
{
|
||||
errors.Add($"Group [{group.GroupId}] 内信号名称重复 | Duplicate signal name in Group [{group.GroupId}]: {dupName}");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 验证所有 Group 中信号名称全局唯一性(跨 Group 检查)| Validate global signal name uniqueness across Groups
|
||||
var allSignals = Groups.SelectMany(g => g.Signals.Select(s => new { Signal = s, GroupId = g.GroupId })).ToList();
|
||||
var crossGroupDuplicates = allSignals
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Signal.Name))
|
||||
.GroupBy(x => x.Signal.Name)
|
||||
.Where(g => g.Select(x => x.GroupId).Distinct().Count() > 1)
|
||||
.ToList();
|
||||
foreach (var dup in crossGroupDuplicates)
|
||||
{
|
||||
var groupIds = string.Join(", ", dup.Select(x => x.GroupId).Distinct());
|
||||
errors.Add($"跨 Group 信号名称重复 | Cross-Group duplicate signal name: {dup.Key} (Groups: {groupIds})");
|
||||
}
|
||||
|
||||
// 5. 检查所有信号条目的字段验证 | Check field validation for all signal entries
|
||||
foreach (var group in Groups)
|
||||
{
|
||||
foreach (var signal in group.Signals.Where(s => !string.IsNullOrEmpty(s.ValidationError)))
|
||||
{
|
||||
errors.Add($"Group [{group.GroupId}] 信号 [{signal.Name}]: {signal.ValidationError}");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var errorMsg = LocalizationHelper.Get("PlcAddrEditor_ValidationFailed") + "\n" + string.Join("\n", errors);
|
||||
MessageBox.Show(errorMsg, LocalizationHelper.Get("PlcAddrEditor_Title"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存信号配置到 XML | Save signal configuration to XML
|
||||
_xmlParser.SaveToFile(XmlFilePath, Groups.ToList());
|
||||
|
||||
// 保存 PlcConfig 参数到 App.config | Save PlcConfig parameters to App.config
|
||||
SavePlcConfig();
|
||||
|
||||
HasUnsavedChanges = false;
|
||||
_logger.Info("信号数据和 PLC 配置保存成功 | Signals and PLC config saved successfully");
|
||||
MessageBox.Show(LocalizationHelper.Get("PlcAddrEditor_SaveSuccess"), LocalizationHelper.Get("PlcAddrEditor_Title"), MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"保存失败 | Save failed: {ex.Message}");
|
||||
MessageBox.Show(LocalizationHelper.Get("PlcAddrEditor_SaveFailed") + $"\n{ex.Message}", LocalizationHelper.Get("PlcAddrEditor_Title"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行取消操作,关闭窗口 | Execute cancel, close window
|
||||
/// </summary>
|
||||
private void ExecuteCancel()
|
||||
{
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行重新加载操作 | Execute reload operation
|
||||
/// 存在未保存修改时提示确认
|
||||
/// </summary>
|
||||
private void ExecuteReload()
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
LocalizationHelper.Get("PlcAddrEditor_ReloadConfirm"),
|
||||
LocalizationHelper.Get("PlcAddrEditor_Title"),
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result != MessageBoxResult.Yes)
|
||||
return;
|
||||
|
||||
LoadSignals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存 PLC 连接参数到 App.config(已移除 WriteDbBlock)| Save PLC connection parameters to App.config (WriteDbBlock removed)
|
||||
/// </summary>
|
||||
private void SavePlcConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
|
||||
var settings = config.AppSettings.Settings;
|
||||
|
||||
void SetOrAdd(string key, string value)
|
||||
{
|
||||
if (settings[key] == null)
|
||||
settings.Add(key, value);
|
||||
else
|
||||
settings[key].Value = value;
|
||||
}
|
||||
|
||||
SetOrAdd("Plc:IpAddress", IpAddress ?? string.Empty);
|
||||
SetOrAdd("Plc:Port", Port.ToString());
|
||||
SetOrAdd("Plc:Rack", Rack.ToString());
|
||||
SetOrAdd("Plc:Slot", Slot.ToString());
|
||||
SetOrAdd("Plc:PlcType", SelectedPlcType.ToString());
|
||||
SetOrAdd("Plc:ReadDbBlock", ReadDbBlock ?? string.Empty);
|
||||
SetOrAdd("Plc:ReadStartAddress", ReadStartAddress.ToString());
|
||||
SetOrAdd("Plc:ReadLength", ReadLength.ToString());
|
||||
SetOrAdd("Plc:BulkReadIntervalMs", BulkReadIntervalMs.ToString());
|
||||
SetOrAdd("Plc:ConnectTimeoutMs", ConnectTimeoutMs.ToString());
|
||||
SetOrAdd("Plc:ReadTimeoutMs", ReadTimeoutMs.ToString());
|
||||
SetOrAdd("Plc:WriteTimeoutMs", WriteTimeoutMs.ToString());
|
||||
SetOrAdd("Plc:bReConnect", BReConnect.ToString());
|
||||
|
||||
config.Save(ConfigurationSaveMode.Modified);
|
||||
ConfigurationManager.RefreshSection("appSettings");
|
||||
_logger.Info("PLC 配置参数已保存到 App.config | PLC config saved to App.config");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn($"保存 PLC 配置到 App.config 失败 | Failed to save PLC config to App.config: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
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;
|
||||
using XP.Hardware.PLC.Helpers;
|
||||
|
||||
namespace XP.Hardware.PLC.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// PLC 测试台 ViewModel | PLC Test Bench ViewModel
|
||||
/// 提供连接管理、单点读写、批量读取和日志功能
|
||||
/// </summary>
|
||||
public class PlcTestBenchViewModel : BindableBase
|
||||
{
|
||||
private readonly PlcService _plcService;
|
||||
private readonly IPlcClient _plcClient;
|
||||
private readonly ConfigLoader _configLoader;
|
||||
private readonly ILoggerService _logger;
|
||||
private CancellationTokenSource _cts;
|
||||
private System.Windows.Threading.DispatcherTimer _continuousReadTimer;
|
||||
|
||||
// === 数据类型选项 | Data type options ===
|
||||
private readonly string[] _dataTypes = { "Bool", "Byte", "Short", "Int", "Float", "Double", "String" };
|
||||
public string[] DataTypes => _dataTypes;
|
||||
|
||||
// === 连接配置区属性 | Connection config properties ===
|
||||
private string _ipAddress = "127.0.0.1";
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
}
|
||||
|
||||
private int _port = 502;
|
||||
public int Port
|
||||
{
|
||||
get => _port;
|
||||
set => SetProperty(ref _port, value);
|
||||
}
|
||||
|
||||
private short _rack = 0;
|
||||
public short Rack
|
||||
{
|
||||
get => _rack;
|
||||
set => SetProperty(ref _rack, value);
|
||||
}
|
||||
|
||||
private short _slot = 1;
|
||||
public short Slot
|
||||
{
|
||||
get => _slot;
|
||||
set => SetProperty(ref _slot, value);
|
||||
}
|
||||
|
||||
private PlcType _selectedPlcType = PlcType.S1200;
|
||||
public PlcType SelectedPlcType
|
||||
{
|
||||
get => _selectedPlcType;
|
||||
set => SetProperty(ref _selectedPlcType, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLC 类型枚举值列表,用于下拉框绑定 | PLC type enum values for ComboBox binding
|
||||
/// </summary>
|
||||
public PlcType[] PlcTypes => Enum.GetValues(typeof(PlcType)).Cast<PlcType>().ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// 连接状态(绑定 PlcService.IsConnected)| Connection status
|
||||
/// </summary>
|
||||
public bool IsConnected => _plcService.IsConnected;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本(绑定 PlcService.StatusText)| Status text
|
||||
/// </summary>
|
||||
public string StatusText => _plcService.StatusText;
|
||||
|
||||
private bool _isConnecting;
|
||||
/// <summary>
|
||||
/// 连接中标志,防止重复点击 | Connecting flag to prevent duplicate clicks
|
||||
/// </summary>
|
||||
public bool IsConnecting
|
||||
{
|
||||
get => _isConnecting;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isConnecting, value))
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
// === 单点读写区属性 | Single point read/write properties ===
|
||||
private string _pointAddress;
|
||||
public string PointAddress
|
||||
{
|
||||
get => _pointAddress;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _pointAddress, value))
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
private string _selectedDataType = "Int";
|
||||
public string SelectedDataType
|
||||
{
|
||||
get => _selectedDataType;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedDataType, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsBoolType));
|
||||
RaisePropertyChanged(nameof(IsStringType));
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _readResult;
|
||||
public string ReadResult
|
||||
{
|
||||
get => _readResult;
|
||||
set => SetProperty(ref _readResult, value);
|
||||
}
|
||||
|
||||
private string _writeValue;
|
||||
public string WriteValue
|
||||
{
|
||||
get => _writeValue;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _writeValue, value))
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 Bool 类型(控制 Toggle 按钮可见性)| Whether data type is Bool
|
||||
/// </summary>
|
||||
public bool IsBoolType => SelectedDataType == "Bool";
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 String 类型(控制字符串长度输入可见性)| Whether data type is String
|
||||
/// </summary>
|
||||
public bool IsStringType => SelectedDataType == "String";
|
||||
|
||||
private int _stringLength = 10;
|
||||
/// <summary>
|
||||
/// 字符串读写长度(字节数)| String read/write length in bytes
|
||||
/// </summary>
|
||||
public int StringLength
|
||||
{
|
||||
get => _stringLength;
|
||||
set => SetProperty(ref _stringLength, value);
|
||||
}
|
||||
|
||||
private bool _isContinuousReading;
|
||||
/// <summary>
|
||||
/// 是否正在连续读取 | Whether continuous reading is active
|
||||
/// </summary>
|
||||
public bool IsContinuousReading
|
||||
{
|
||||
get => _isContinuousReading;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isContinuousReading, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsNotContinuousReading));
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否未在连续读取(用于按钮启用绑定)| Whether not continuous reading
|
||||
/// </summary>
|
||||
public bool IsNotContinuousReading => !IsContinuousReading;
|
||||
|
||||
// === 批量读取区属性 | Batch read properties ===
|
||||
private string _batchAddress;
|
||||
public string BatchAddress
|
||||
{
|
||||
get => _batchAddress;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _batchAddress, value))
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
private string _batchLength;
|
||||
public string BatchLength
|
||||
{
|
||||
get => _batchLength;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _batchLength, value))
|
||||
RefreshCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
private string _hexViewerContent;
|
||||
public string HexViewerContent
|
||||
{
|
||||
get => _hexViewerContent;
|
||||
set => SetProperty(ref _hexViewerContent, value);
|
||||
}
|
||||
|
||||
// === 日志区属性 | Log properties ===
|
||||
public ObservableCollection<LogEntry> LogEntries { get; } = new ObservableCollection<LogEntry>();
|
||||
|
||||
// === 命令 | Commands ===
|
||||
public DelegateCommand ConnectCommand { get; }
|
||||
public DelegateCommand DisconnectCommand { get; }
|
||||
public DelegateCommand ReadCommand { get; }
|
||||
public DelegateCommand WriteCommand { get; }
|
||||
public DelegateCommand ToggleBoolCommand { get; }
|
||||
public DelegateCommand BatchReadCommand { get; }
|
||||
public DelegateCommand ClearLogCommand { get; }
|
||||
public DelegateCommand StartContinuousReadCommand { get; }
|
||||
public DelegateCommand StopContinuousReadCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="plcClient">PLC 客户端接口 | PLC client interface</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public PlcTestBenchViewModel(IPlcClient plcClient, ILoggerService logger)
|
||||
{
|
||||
_plcClient = plcClient ?? throw new ArgumentNullException(nameof(plcClient));
|
||||
_logger = logger?.ForModule<PlcTestBenchViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_plcService = new PlcService(plcClient, logger, new XmlSignalParser());
|
||||
_configLoader = new ConfigLoader(logger);
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
// 监听 PlcService 属性变化,转发到 UI | Forward PlcService property changes to UI
|
||||
_plcService.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(PlcService.IsConnected))
|
||||
{
|
||||
RaisePropertyChanged(nameof(IsConnected));
|
||||
RefreshCommandStates();
|
||||
}
|
||||
else if (e.PropertyName == nameof(PlcService.StatusText))
|
||||
{
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化命令 | Initialize commands
|
||||
ConnectCommand = new DelegateCommand(async () => await ConnectAsync())
|
||||
.ObservesCanExecute(() => CanConnect);
|
||||
DisconnectCommand = new DelegateCommand(Disconnect)
|
||||
.ObservesCanExecute(() => IsConnected);
|
||||
ReadCommand = new DelegateCommand(async () => await ReadPointAsync(),
|
||||
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress));
|
||||
WriteCommand = new DelegateCommand(async () => await WritePointAsync(),
|
||||
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress) && !string.IsNullOrWhiteSpace(WriteValue));
|
||||
ToggleBoolCommand = new DelegateCommand(async () => await ToggleBoolAsync(),
|
||||
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress) && IsBoolType);
|
||||
BatchReadCommand = new DelegateCommand(async () => await BatchReadAsync(),
|
||||
() => IsConnected && !string.IsNullOrWhiteSpace(BatchAddress) && !string.IsNullOrWhiteSpace(BatchLength));
|
||||
ClearLogCommand = new DelegateCommand(() => LogEntries.Clear());
|
||||
StartContinuousReadCommand = new DelegateCommand(StartContinuousRead,
|
||||
() => IsConnected && !string.IsNullOrWhiteSpace(PointAddress) && !IsContinuousReading);
|
||||
StopContinuousReadCommand = new DelegateCommand(StopContinuousRead,
|
||||
() => IsContinuousReading);
|
||||
|
||||
// 初始化连续读取定时器(250ms 间隔)| Initialize continuous read timer (250ms interval)
|
||||
_continuousReadTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(250)
|
||||
};
|
||||
_continuousReadTimer.Tick += async (s, e) => await ContinuousReadTickAsync();
|
||||
|
||||
// 加载配置 | Load configuration
|
||||
LoadConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接按钮启用条件 | Connect button enable condition
|
||||
/// </summary>
|
||||
public bool CanConnect => !IsConnecting && !IsConnected;
|
||||
|
||||
/// <summary>
|
||||
/// 加载 PLC 配置 | Load PLC configuration
|
||||
/// </summary>
|
||||
private void LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = _configLoader.LoadPlcConfig();
|
||||
IpAddress = config.IpAddress;
|
||||
Port = config.Port;
|
||||
Rack = config.Rack;
|
||||
Slot = config.Slot;
|
||||
SelectedPlcType = config.PlcType;
|
||||
AddLog(TestBenchLogLevel.INFO, "配置加载成功 | Configuration loaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 使用默认值 | Use default values
|
||||
IpAddress = "127.0.0.1";
|
||||
Port = 502;
|
||||
Rack = 0;
|
||||
Slot = 1;
|
||||
SelectedPlcType = PlcType.S1200;
|
||||
AddLog(TestBenchLogLevel.ERROR, $"配置加载失败,使用默认值 | Configuration load failed, using defaults: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加日志条目 | Add log entry
|
||||
/// </summary>
|
||||
public void AddLog(TestBenchLogLevel level, string message)
|
||||
{
|
||||
var entry = new LogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now.ToString("HH:mm:ss.fff"),
|
||||
Level = level,
|
||||
Message = message
|
||||
};
|
||||
LogEntries.Add(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新所有命令的启用状态 | Refresh all command enable states
|
||||
/// </summary>
|
||||
private void RefreshCommandStates()
|
||||
{
|
||||
RaisePropertyChanged(nameof(CanConnect));
|
||||
ReadCommand.RaiseCanExecuteChanged();
|
||||
WriteCommand.RaiseCanExecuteChanged();
|
||||
ToggleBoolCommand.RaiseCanExecuteChanged();
|
||||
BatchReadCommand.RaiseCanExecuteChanged();
|
||||
StartContinuousReadCommand.RaiseCanExecuteChanged();
|
||||
StopContinuousReadCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接 PLC | Connect to PLC
|
||||
/// </summary>
|
||||
private async Task ConnectAsync()
|
||||
{
|
||||
IsConnecting = true;
|
||||
try
|
||||
{
|
||||
var config = new PlcConfig
|
||||
{
|
||||
IpAddress = IpAddress,
|
||||
Port = Port,
|
||||
Rack = Rack,
|
||||
Slot = Slot,
|
||||
PlcType = SelectedPlcType
|
||||
};
|
||||
|
||||
AddLog(TestBenchLogLevel.INFO, $"正在连接 PLC {IpAddress}:{Port} ... | Connecting to PLC...");
|
||||
var result = await _plcService.InitializeAsync(config);
|
||||
|
||||
if (result)
|
||||
AddLog(TestBenchLogLevel.INFO, "PLC 连接成功 | PLC connected successfully");
|
||||
else
|
||||
AddLog(TestBenchLogLevel.WARN, "PLC 连接失败 | PLC connection failed");
|
||||
}
|
||||
catch (PlcException ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"连接失败: {ex.Message} | Connection failed: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"连接异常: {ex.Message} | Connection exception: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开 PLC 连接 | Disconnect PLC
|
||||
/// </summary>
|
||||
private void Disconnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
_plcService.Dispose();
|
||||
AddLog(TestBenchLogLevel.INFO, "PLC 已断开 | PLC disconnected");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"断开失败: {ex.Message} | Disconnect failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取单点数据 | Read single point data
|
||||
/// </summary>
|
||||
private async Task ReadPointAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PointAddress))
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object result = SelectedDataType switch
|
||||
{
|
||||
"Bool" => await _plcService.ReadValueAsync<bool>(PointAddress),
|
||||
"Byte" => await _plcService.ReadValueAsync<byte>(PointAddress),
|
||||
"Short" => await _plcService.ReadValueAsync<short>(PointAddress),
|
||||
"Int" => await _plcService.ReadValueAsync<int>(PointAddress),
|
||||
"Float" => await _plcService.ReadValueAsync<float>(PointAddress),
|
||||
"Double" => await _plcService.ReadValueAsync<double>(PointAddress),
|
||||
"String" => await _plcService.ReadStringAsync(PointAddress, (ushort)StringLength),
|
||||
_ => throw new InvalidOperationException($"不支持的数据类型: {SelectedDataType}")
|
||||
};
|
||||
|
||||
ReadResult = result?.ToString() ?? "null";
|
||||
AddLog(TestBenchLogLevel.INFO, $"读取成功: 地址={PointAddress}, 类型={SelectedDataType}, 值={ReadResult} | Read success");
|
||||
}
|
||||
catch (PlcException ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"读取失败: 地址={PointAddress}, 错误={ex.Message} | Read failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"读取异常: {ex.Message} | Read exception");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入单点数据 | Write single point data
|
||||
/// </summary>
|
||||
private async Task WritePointAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PointAddress))
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(WriteValue))
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, "写入值不能为空 | Write value cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var converted = ConvertToType(WriteValue, SelectedDataType);
|
||||
if (converted == null)
|
||||
return;
|
||||
|
||||
bool result = SelectedDataType switch
|
||||
{
|
||||
"Bool" => await _plcService.WriteValueAsync(PointAddress, (bool)converted),
|
||||
"Byte" => await _plcService.WriteValueAsync(PointAddress, (byte)converted),
|
||||
"Short" => await _plcService.WriteValueAsync(PointAddress, (short)converted),
|
||||
"Int" => await _plcService.WriteValueAsync(PointAddress, (int)converted),
|
||||
"Float" => await _plcService.WriteValueAsync(PointAddress, (float)converted),
|
||||
"Double" => await _plcService.WriteValueAsync(PointAddress, (double)converted),
|
||||
"String" => await _plcService.WriteStringAsync(PointAddress, (string)converted, (ushort)StringLength),
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (result)
|
||||
AddLog(TestBenchLogLevel.INFO, $"写入成功: 地址={PointAddress}, 类型={SelectedDataType}, 值={WriteValue} | Write success");
|
||||
else
|
||||
AddLog(TestBenchLogLevel.ERROR, $"写入失败: 地址={PointAddress} | Write failed");
|
||||
}
|
||||
catch (PlcException ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"写入失败: 地址={PointAddress}, 错误={ex.Message} | Write failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"写入异常: {ex.Message} | Write exception");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将字符串转换为指定数据类型 | Convert string to specified data type
|
||||
/// </summary>
|
||||
private object ConvertToType(string value, string dataType)
|
||||
{
|
||||
try
|
||||
{
|
||||
return dataType switch
|
||||
{
|
||||
"Bool" => bool.Parse(value),
|
||||
"Byte" => byte.Parse(value),
|
||||
"Short" => short.Parse(value),
|
||||
"Int" => int.Parse(value),
|
||||
"Float" => float.Parse(value),
|
||||
"Double" => double.Parse(value),
|
||||
"String" => value,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"类型转换失败: 无法将 '{value}' 转换为 {dataType} | Type conversion failed");
|
||||
return null;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"类型转换失败: '{value}' 超出 {dataType} 范围 | Value overflow");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换 Bool 值 | Toggle bool value
|
||||
/// </summary>
|
||||
private async Task ToggleBoolAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PointAddress))
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var currentValue = await _plcService.ReadValueAsync<bool>(PointAddress);
|
||||
var newValue = !currentValue;
|
||||
var result = await _plcService.WriteValueAsync(PointAddress, newValue);
|
||||
|
||||
if (result)
|
||||
{
|
||||
ReadResult = newValue.ToString();
|
||||
AddLog(TestBenchLogLevel.INFO, $"切换成功: 地址={PointAddress}, {currentValue} → {newValue} | Toggle success");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"切换失败: 地址={PointAddress} | Toggle failed");
|
||||
}
|
||||
}
|
||||
catch (PlcException ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"切换失败: 地址={PointAddress}, 错误={ex.Message} | Toggle failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"切换异常: {ex.Message} | Toggle exception");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量读取数据 | Batch read data
|
||||
/// </summary>
|
||||
private async Task BatchReadAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BatchAddress))
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, "起始地址不能为空 | Start address cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(BatchLength, out int length) || length <= 0)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"字节长度无效: {BatchLength} | Invalid byte length");
|
||||
return;
|
||||
}
|
||||
|
||||
var parsed = ParseBatchAddress(BatchAddress);
|
||||
if (parsed == null)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"地址格式错误: {BatchAddress},正确格式为 DB{{N}}.{{StartAddress}} | Invalid address format");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var (dbBlock, startAddress) = parsed.Value;
|
||||
var data = await _plcClient.ReadBytesAsync(dbBlock, startAddress, length);
|
||||
HexViewerContent = HexFormatter.Format(data, startAddress);
|
||||
AddLog(TestBenchLogLevel.INFO, $"批量读取成功: {dbBlock}.{startAddress}, 长度={length} 字节 | Batch read success");
|
||||
}
|
||||
catch (PlcException ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"批量读取失败: {ex.Message} | Batch read failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"批量读取异常: {ex.Message} | Batch read exception");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析批量读取地址 | Parse batch read address
|
||||
/// 输入格式:"DB{N}.{StartAddress}"
|
||||
/// </summary>
|
||||
private (string dbBlock, int startAddress)? ParseBatchAddress(string address)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
return null;
|
||||
|
||||
var dotIndex = address.IndexOf('.');
|
||||
if (dotIndex <= 0 || dotIndex >= address.Length - 1)
|
||||
return null;
|
||||
|
||||
var dbBlock = address.Substring(0, dotIndex);
|
||||
var startPart = address.Substring(dotIndex + 1);
|
||||
|
||||
if (!dbBlock.StartsWith("DB", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (!int.TryParse(startPart, out int startAddress))
|
||||
return null;
|
||||
|
||||
return (dbBlock, startAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始连续读取 | Start continuous reading
|
||||
/// </summary>
|
||||
private void StartContinuousRead()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PointAddress))
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, "地址不能为空 | Address cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
IsContinuousReading = true;
|
||||
_continuousReadTimer.Start();
|
||||
AddLog(TestBenchLogLevel.INFO, $"开始连续读取: 地址={PointAddress}, 类型={SelectedDataType}, 间隔=250ms | Continuous read started");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止连续读取 | Stop continuous reading
|
||||
/// </summary>
|
||||
private void StopContinuousRead()
|
||||
{
|
||||
_continuousReadTimer.Stop();
|
||||
IsContinuousReading = false;
|
||||
AddLog(TestBenchLogLevel.INFO, "连续读取已停止 | Continuous read stopped");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连续读取定时器回调 | Continuous read timer tick callback
|
||||
/// </summary>
|
||||
private async Task ContinuousReadTickAsync()
|
||||
{
|
||||
if (!IsConnected || string.IsNullOrWhiteSpace(PointAddress))
|
||||
{
|
||||
StopContinuousRead();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object result = SelectedDataType switch
|
||||
{
|
||||
"Bool" => await _plcService.ReadValueAsync<bool>(PointAddress),
|
||||
"Byte" => await _plcService.ReadValueAsync<byte>(PointAddress),
|
||||
"Short" => await _plcService.ReadValueAsync<short>(PointAddress),
|
||||
"Int" => await _plcService.ReadValueAsync<int>(PointAddress),
|
||||
"Float" => await _plcService.ReadValueAsync<float>(PointAddress),
|
||||
"Double" => await _plcService.ReadValueAsync<double>(PointAddress),
|
||||
"String" => await _plcService.ReadStringAsync(PointAddress, (ushort)StringLength),
|
||||
_ => throw new InvalidOperationException($"不支持的数据类型: {SelectedDataType}")
|
||||
};
|
||||
|
||||
ReadResult = result?.ToString() ?? "null";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(TestBenchLogLevel.ERROR, $"连续读取异常: {ex.Message} | Continuous read exception");
|
||||
StopContinuousRead();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源 | Cleanup resources
|
||||
/// 窗口关闭时调用,取消异步操作并释放 PlcService
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
StopContinuousRead();
|
||||
}
|
||||
catch { /* 静默处理 | Silent handling */ }
|
||||
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
catch { /* 静默处理 | Silent handling */ }
|
||||
|
||||
try
|
||||
{
|
||||
_plcService?.Dispose();
|
||||
}
|
||||
catch { /* 静默处理 | Silent handling */ }
|
||||
|
||||
try
|
||||
{
|
||||
_cts?.Dispose();
|
||||
}
|
||||
catch { /* 静默处理 | Silent handling */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user