1
This commit is contained in:
1 parent
99c2c59552
commit
012c9522f8
5 files changed
+1717
-106
No files matched your search
@@ -1,34 +1,46 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
using Sharp.Wpf.Navigation;
|
using Sharp.Wpf.Navigation;
|
||||||
|
using Sharp.Wpf.Abstractions;
|
||||||
using Plc.Abstractions;
|
using Plc.Abstractions;
|
||||||
|
using Plc.Models;
|
||||||
|
|
||||||
namespace Sharp.Demo.ViewModels.Pages
|
namespace Sharp.Demo.ViewModels.Pages
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PLC 调试页面 ViewModel - 简化版
|
/// PLC 调试页面 ViewModel - 完整功能版
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
public class PlcDebugViewModel : ViewModelBase, INavigationAware
|
||||||
{
|
{
|
||||||
private readonly IPlcService _plcService;
|
private readonly IPlcService _plcService;
|
||||||
private readonly ILogger<PlcDebugViewModel> _logger;
|
private readonly ILogger<PlcDebugViewModel> _logger;
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
private string _ipAddress = "192.168.1.100";
|
private string _ipAddress = "192.168.1.100";
|
||||||
private string _status = "未连接";
|
private string _status = "未连接";
|
||||||
private string _readAddress = "DB1.DBW0";
|
private string _readAddress = "DB1.DBW0";
|
||||||
private string _readValue = "-";
|
private string _readValue = "-";
|
||||||
|
private Type _readDataType = typeof(short);
|
||||||
private string _writeAddress = "DB1.DBW0";
|
private string _writeAddress = "DB1.DBW0";
|
||||||
private string _writeValue = "0";
|
private string _writeValue = "0";
|
||||||
|
private Type _writeDataType = typeof(short);
|
||||||
private string _logText = string.Empty;
|
private string _logText = string.Empty;
|
||||||
|
private string _monitorAddress = "DB1.DBW0";
|
||||||
|
private Type _monitorDataType = typeof(short);
|
||||||
|
|
||||||
public PlcDebugViewModel(IPlcService plcService, ILogger<PlcDebugViewModel> logger)
|
public PlcDebugViewModel(
|
||||||
|
IPlcService plcService,
|
||||||
|
ILogger<PlcDebugViewModel> logger,
|
||||||
|
IDispatcher dispatcher)
|
||||||
{
|
{
|
||||||
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
|
|
||||||
// 初始化命令
|
// 初始化命令
|
||||||
ConnectCommand = new AsyncCommand(ConnectAsync);
|
ConnectCommand = new AsyncCommand(ConnectAsync);
|
||||||
@@ -36,6 +48,14 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
ReadCommand = new AsyncCommand(ReadAsync);
|
ReadCommand = new AsyncCommand(ReadAsync);
|
||||||
WriteCommand = new AsyncCommand(WriteAsync);
|
WriteCommand = new AsyncCommand(WriteAsync);
|
||||||
ClearLogCommand = new RelayCommand(_ => ClearLog());
|
ClearLogCommand = new RelayCommand(_ => ClearLog());
|
||||||
|
AddMonitorCommand = new RelayCommand(_ => AddMonitor());
|
||||||
|
RemoveMonitorCommand = new RelayCommand<PlcTagViewModel>(RemoveMonitor, vm => vm != null);
|
||||||
|
AddToQueueCommand = new RelayCommand(_ => AddToQueue());
|
||||||
|
RemoveFromQueueCommand = new RelayCommand<WriteQueueItem>(RemoveFromQueue, item => item != null);
|
||||||
|
ExecuteQueueCommand = new AsyncCommand(ExecuteQueueAsync);
|
||||||
|
ClearQueueCommand = new RelayCommand(_ => WriteQueue.Clear());
|
||||||
|
MoveUpQueueCommand = new RelayCommand<WriteQueueItem>(MoveUpQueue, item => item != null);
|
||||||
|
MoveDownQueueCommand = new RelayCommand<WriteQueueItem>(MoveDownQueue, item => item != null);
|
||||||
|
|
||||||
AppendLog("PLC 调试工具已加载");
|
AppendLog("PLC 调试工具已加载");
|
||||||
AppendLog("提示:需要配置真实的 PLC 连接信息才能使用");
|
AppendLog("提示:需要配置真实的 PLC 连接信息才能使用");
|
||||||
@@ -67,6 +87,12 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
set => SetProperty(ref _readValue, value);
|
set => SetProperty(ref _readValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Type ReadDataType
|
||||||
|
{
|
||||||
|
get => _readDataType;
|
||||||
|
set => SetProperty(ref _readDataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
public string WriteAddress
|
public string WriteAddress
|
||||||
{
|
{
|
||||||
get => _writeAddress;
|
get => _writeAddress;
|
||||||
@@ -79,12 +105,58 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
set => SetProperty(ref _writeValue, value);
|
set => SetProperty(ref _writeValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Type WriteDataType
|
||||||
|
{
|
||||||
|
get => _writeDataType;
|
||||||
|
set => SetProperty(ref _writeDataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
public string LogText
|
public string LogText
|
||||||
{
|
{
|
||||||
get => _logText;
|
get => _logText;
|
||||||
set => SetProperty(ref _logText, value);
|
set => SetProperty(ref _logText, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string MonitorAddress
|
||||||
|
{
|
||||||
|
get => _monitorAddress;
|
||||||
|
set => SetProperty(ref _monitorAddress, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type MonitorDataType
|
||||||
|
{
|
||||||
|
get => _monitorDataType;
|
||||||
|
set => SetProperty(ref _monitorDataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持的数据类型
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<DataTypeOption> SupportedDataTypes { get; } = new()
|
||||||
|
{
|
||||||
|
new("Bool", typeof(bool)),
|
||||||
|
new("Byte", typeof(byte)),
|
||||||
|
new("Short (Int16)", typeof(short)),
|
||||||
|
new("UShort (UInt16)", typeof(ushort)),
|
||||||
|
new("Int (Int32)", typeof(int)),
|
||||||
|
new("UInt (UInt32)", typeof(uint)),
|
||||||
|
new("Long (Int64)", typeof(long)),
|
||||||
|
new("ULong (UInt64)", typeof(ulong)),
|
||||||
|
new("Float (Real)", typeof(float)),
|
||||||
|
new("Double (LReal)", typeof(double)),
|
||||||
|
new("String", typeof(string))
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 监控列表
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入队列
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<WriteQueueItem> WriteQueue { get; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
@@ -94,6 +166,14 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
public ICommand ReadCommand { get; }
|
public ICommand ReadCommand { get; }
|
||||||
public ICommand WriteCommand { get; }
|
public ICommand WriteCommand { get; }
|
||||||
public ICommand ClearLogCommand { get; }
|
public ICommand ClearLogCommand { get; }
|
||||||
|
public ICommand AddMonitorCommand { get; }
|
||||||
|
public ICommand RemoveMonitorCommand { get; }
|
||||||
|
public ICommand AddToQueueCommand { get; }
|
||||||
|
public ICommand RemoveFromQueueCommand { get; }
|
||||||
|
public ICommand ExecuteQueueCommand { get; }
|
||||||
|
public ICommand ClearQueueCommand { get; }
|
||||||
|
public ICommand MoveUpQueueCommand { get; }
|
||||||
|
public ICommand MoveDownQueueCommand { get; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -153,18 +233,36 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AppendLog($"读取地址: {ReadAddress}");
|
AppendLog($"读取地址: {ReadAddress}, 类型: {GetTypeName(ReadDataType)}");
|
||||||
var result = await _plcService.ReadAsync<short>(ReadAddress);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
// 使用反射调用泛型方法
|
||||||
|
var method = _plcService.GetType().GetMethod(nameof(IPlcService.ReadAsync))!
|
||||||
|
.MakeGenericMethod(ReadDataType);
|
||||||
|
|
||||||
|
var task = (Task)method.Invoke(_plcService, new object[] { ReadAddress })!;
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 获取 Result 属性
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result")!;
|
||||||
|
var result = resultProperty.GetValue(task);
|
||||||
|
|
||||||
|
// 检查 IsSuccess
|
||||||
|
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
|
||||||
|
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
ReadValue = result.Value.ToString();
|
var valueProperty = result.GetType().GetProperty("Value")!;
|
||||||
|
var value = valueProperty.GetValue(result);
|
||||||
|
ReadValue = value?.ToString() ?? "null";
|
||||||
AppendLog($"✅ 读取成功: {ReadValue}");
|
AppendLog($"✅ 读取成功: {ReadValue}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
|
||||||
|
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
|
||||||
ReadValue = "读取失败";
|
ReadValue = "读取失败";
|
||||||
AppendLog($"❌ 读取失败: {result.ErrorMessage}");
|
AppendLog($"❌ 读取失败: {errorMessage}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -179,22 +277,38 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!short.TryParse(WriteValue, out short value))
|
// 解析值
|
||||||
|
var value = ParseValue(WriteValue, WriteDataType);
|
||||||
|
if (value == null)
|
||||||
{
|
{
|
||||||
AppendLog($"❌ 写入失败: 值格式不正确");
|
AppendLog($"❌ 写入失败: 值格式不正确");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppendLog($"写入地址: {WriteAddress} = {value}");
|
AppendLog($"写入地址: {WriteAddress} = {value}, 类型: {GetTypeName(WriteDataType)}");
|
||||||
var result = await _plcService.WriteAsync(WriteAddress, value);
|
|
||||||
|
|
||||||
if (result.IsSuccess)
|
// 使用反射调用泛型方法
|
||||||
|
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
|
||||||
|
var task = (Task)method.Invoke(_plcService, new object[] { WriteAddress, value })!;
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 获取 Result 属性
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result")!;
|
||||||
|
var result = resultProperty.GetValue(task);
|
||||||
|
|
||||||
|
// 检查 IsSuccess
|
||||||
|
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
|
||||||
|
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
AppendLog($"✅ 写入成功");
|
AppendLog($"✅ 写入成功");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AppendLog($"❌ 写入失败: {result.ErrorMessage}");
|
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
|
||||||
|
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
|
||||||
|
AppendLog($"❌ 写入失败: {errorMessage}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -216,6 +330,326 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
LogText += $"[{timestamp}] {message}\n";
|
LogText += $"[{timestamp}] {message}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加监控
|
||||||
|
/// </summary>
|
||||||
|
private void AddMonitor()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tag = new PlcTag
|
||||||
|
{
|
||||||
|
Address = MonitorAddress,
|
||||||
|
DataType = MonitorDataType
|
||||||
|
};
|
||||||
|
|
||||||
|
var vm = new PlcTagViewModel(tag);
|
||||||
|
|
||||||
|
tag.OnChanged = (t, v) =>
|
||||||
|
{
|
||||||
|
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
vm.UpdateValue();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_plcService.Subscribe(tag);
|
||||||
|
|
||||||
|
// 在UI线程添加到集合
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
MonitoredTags.Add(vm);
|
||||||
|
});
|
||||||
|
|
||||||
|
AppendLog($"✅ 添加监控: {MonitorAddress} ({GetTypeName(MonitorDataType)})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 添加监控失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "添加监控失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除监控
|
||||||
|
/// </summary>
|
||||||
|
private void RemoveMonitor(PlcTagViewModel? tagVm)
|
||||||
|
{
|
||||||
|
if (tagVm == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从PLC服务取消订阅
|
||||||
|
_plcService.Unsubscribe(tagVm.Id);
|
||||||
|
|
||||||
|
// 从UI集合移除
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
MonitoredTags.Remove(tagVm);
|
||||||
|
});
|
||||||
|
|
||||||
|
AppendLog($"✅ 移除监控: {tagVm.Address}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 移除监控失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "移除监控失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加到写入队列
|
||||||
|
/// </summary>
|
||||||
|
private void AddToQueue()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = ParseValue(WriteValue, WriteDataType);
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 添加到队列失败: 值格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new WriteQueueItem
|
||||||
|
{
|
||||||
|
Order = WriteQueue.Count + 1,
|
||||||
|
Address = WriteAddress,
|
||||||
|
Value = value,
|
||||||
|
DataType = WriteDataType,
|
||||||
|
DataTypeName = GetTypeName(WriteDataType)
|
||||||
|
};
|
||||||
|
|
||||||
|
WriteQueue.Add(item);
|
||||||
|
AppendLog($"✅ 添加到队列: {WriteAddress} = {value} ({GetTypeName(WriteDataType)})");
|
||||||
|
|
||||||
|
// 更新序号
|
||||||
|
UpdateQueueOrder();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 添加到队列失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "添加到队列失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从队列移除
|
||||||
|
/// </summary>
|
||||||
|
private void RemoveFromQueue(WriteQueueItem? item)
|
||||||
|
{
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
WriteQueue.Remove(item);
|
||||||
|
UpdateQueueOrder();
|
||||||
|
AppendLog($"移除队列项: {item.Address}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行写入队列
|
||||||
|
/// </summary>
|
||||||
|
private async Task ExecuteQueueAsync()
|
||||||
|
{
|
||||||
|
if (WriteQueue.Count == 0)
|
||||||
|
{
|
||||||
|
AppendLog("⚠️ 队列为空,无需执行");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLog($"开始执行写入队列 ({WriteQueue.Count} 项)...");
|
||||||
|
|
||||||
|
int successCount = 0;
|
||||||
|
int failCount = 0;
|
||||||
|
|
||||||
|
foreach (var item in WriteQueue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppendLog($"[{item.Order}] 写入: {item.Address} = {item.Value}");
|
||||||
|
|
||||||
|
// 使用反射调用泛型方法
|
||||||
|
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
|
||||||
|
var task = (Task)method.Invoke(_plcService, new object[] { item.Address, item.Value })!;
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 获取 Result 属性
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result")!;
|
||||||
|
var result = resultProperty.GetValue(task);
|
||||||
|
|
||||||
|
// 检查 IsSuccess
|
||||||
|
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
|
||||||
|
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
successCount++;
|
||||||
|
AppendLog($" ✅ 成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failCount++;
|
||||||
|
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
|
||||||
|
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
|
||||||
|
AppendLog($" ❌ 失败: {errorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failCount++;
|
||||||
|
AppendLog($" ❌ 异常: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "写入队列项失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLog($"队列执行完成: 成功 {successCount}, 失败 {failCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列项上移
|
||||||
|
/// </summary>
|
||||||
|
private void MoveUpQueue(WriteQueueItem? item)
|
||||||
|
{
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
var index = WriteQueue.IndexOf(item);
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
WriteQueue.Move(index, index - 1);
|
||||||
|
UpdateQueueOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列项下移
|
||||||
|
/// </summary>
|
||||||
|
private void MoveDownQueue(WriteQueueItem? item)
|
||||||
|
{
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
var index = WriteQueue.IndexOf(item);
|
||||||
|
if (index < WriteQueue.Count - 1)
|
||||||
|
{
|
||||||
|
WriteQueue.Move(index, index + 1);
|
||||||
|
UpdateQueueOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新队列序号
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateQueueOrder()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < WriteQueue.Count; i++)
|
||||||
|
{
|
||||||
|
WriteQueue[i].Order = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析值
|
||||||
|
/// </summary>
|
||||||
|
private object? ParseValue(string valueStr, Type type)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (type == typeof(bool))
|
||||||
|
return bool.Parse(valueStr);
|
||||||
|
if (type == typeof(byte))
|
||||||
|
return byte.Parse(valueStr);
|
||||||
|
if (type == typeof(short))
|
||||||
|
return short.Parse(valueStr);
|
||||||
|
if (type == typeof(ushort))
|
||||||
|
return ushort.Parse(valueStr);
|
||||||
|
if (type == typeof(int))
|
||||||
|
return int.Parse(valueStr);
|
||||||
|
if (type == typeof(uint))
|
||||||
|
return uint.Parse(valueStr);
|
||||||
|
if (type == typeof(long))
|
||||||
|
return long.Parse(valueStr);
|
||||||
|
if (type == typeof(ulong))
|
||||||
|
return ulong.Parse(valueStr);
|
||||||
|
if (type == typeof(float))
|
||||||
|
return float.Parse(valueStr);
|
||||||
|
if (type == typeof(double))
|
||||||
|
return double.Parse(valueStr);
|
||||||
|
if (type == typeof(string))
|
||||||
|
return valueStr;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取类型名称
|
||||||
|
/// </summary>
|
||||||
|
private string GetTypeName(Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(bool)) return "Bool";
|
||||||
|
if (type == typeof(byte)) return "Byte";
|
||||||
|
if (type == typeof(short)) return "Short";
|
||||||
|
if (type == typeof(ushort)) return "UShort";
|
||||||
|
if (type == typeof(int)) return "Int";
|
||||||
|
if (type == typeof(uint)) return "UInt";
|
||||||
|
if (type == typeof(long)) return "Long";
|
||||||
|
if (type == typeof(ulong)) return "ULong";
|
||||||
|
if (type == typeof(float)) return "Float";
|
||||||
|
if (type == typeof(double)) return "Double";
|
||||||
|
if (type == typeof(string)) return "String";
|
||||||
|
return type.Name;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据类型选项
|
||||||
|
/// </summary>
|
||||||
|
public record DataTypeOption(string Name, Type Type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入队列项
|
||||||
|
/// </summary>
|
||||||
|
public class WriteQueueItem : ViewModelBase
|
||||||
|
{
|
||||||
|
private int _order;
|
||||||
|
private string _address = string.Empty;
|
||||||
|
private object _value = 0;
|
||||||
|
private Type _dataType = typeof(short);
|
||||||
|
private string _dataTypeName = "Short";
|
||||||
|
|
||||||
|
public int Order
|
||||||
|
{
|
||||||
|
get => _order;
|
||||||
|
set => SetProperty(ref _order, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Address
|
||||||
|
{
|
||||||
|
get => _address;
|
||||||
|
set => SetProperty(ref _address, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Value
|
||||||
|
{
|
||||||
|
get => _value;
|
||||||
|
set => SetProperty(ref _value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type DataType
|
||||||
|
{
|
||||||
|
get => _dataType;
|
||||||
|
set => SetProperty(ref _dataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DataTypeName
|
||||||
|
{
|
||||||
|
get => _dataTypeName;
|
||||||
|
set => SetProperty(ref _dataTypeName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Plc.Models;
|
||||||
|
using Sharp.Wpf.Mvvm;
|
||||||
|
|
||||||
|
namespace Sharp.Demo.ViewModels.Pages
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PLC 标签 ViewModel - 用于监控列表显示
|
||||||
|
/// </summary>
|
||||||
|
public class PlcTagViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly PlcTag _tag;
|
||||||
|
|
||||||
|
public PlcTagViewModel(PlcTag tag)
|
||||||
|
{
|
||||||
|
_tag = tag ?? throw new ArgumentNullException(nameof(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标签 ID
|
||||||
|
/// </summary>
|
||||||
|
public string Id => _tag.Id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PLC 地址
|
||||||
|
/// </summary>
|
||||||
|
public string Address => _tag.Address;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据类型
|
||||||
|
/// </summary>
|
||||||
|
public Type DataType => _tag.DataType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据类型名称
|
||||||
|
/// </summary>
|
||||||
|
public string DataTypeName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_tag.DataType == typeof(bool)) return "Bool";
|
||||||
|
if (_tag.DataType == typeof(byte)) return "Byte";
|
||||||
|
if (_tag.DataType == typeof(short)) return "Short";
|
||||||
|
if (_tag.DataType == typeof(ushort)) return "UShort";
|
||||||
|
if (_tag.DataType == typeof(int)) return "Int";
|
||||||
|
if (_tag.DataType == typeof(uint)) return "UInt";
|
||||||
|
if (_tag.DataType == typeof(long)) return "Long";
|
||||||
|
if (_tag.DataType == typeof(ulong)) return "ULong";
|
||||||
|
if (_tag.DataType == typeof(float)) return "Float";
|
||||||
|
if (_tag.DataType == typeof(double)) return "Double";
|
||||||
|
if (_tag.DataType == typeof(string)) return "String";
|
||||||
|
return _tag.DataType.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前值
|
||||||
|
/// </summary>
|
||||||
|
public object? Value => _tag.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 底层标签对象
|
||||||
|
/// </summary>
|
||||||
|
public PlcTag Tag => _tag;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新值(通知UI刷新)
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateValue()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,67 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
|
using Sharp.Wpf.Abstractions;
|
||||||
|
using Plc.Abstractions;
|
||||||
|
using Plc.Models;
|
||||||
|
|
||||||
namespace Sharp.Demo.ViewModels.Windows
|
namespace Sharp.Demo.ViewModels.Windows
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PLC 调试窗口 ViewModel - 完整功能版
|
||||||
|
/// </summary>
|
||||||
public class PlcDebugViewModel : ViewModelBase
|
public class PlcDebugViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private string _ipAddress = "127.0.0.1";
|
private readonly IPlcService _plcService;
|
||||||
|
private readonly ILogger<PlcDebugViewModel> _logger;
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
|
private string _ipAddress = "192.168.1.100";
|
||||||
private string _status = "未连接";
|
private string _status = "未连接";
|
||||||
private string _readAddress = "DB1.DBW0";
|
private string _readAddress = "DB1.DBW0";
|
||||||
private string _readValue = "";
|
private string _readValue = "-";
|
||||||
|
private Type _readDataType = typeof(short);
|
||||||
private string _writeAddress = "DB1.DBW0";
|
private string _writeAddress = "DB1.DBW0";
|
||||||
private string _writeValue = "";
|
private string _writeValue = "0";
|
||||||
private string _logText = "等待连接PLC...\n";
|
private Type _writeDataType = typeof(short);
|
||||||
|
private string _lastLogMessage = "等待操作...";
|
||||||
|
private string _monitorAddress = "DB1.DBW0";
|
||||||
|
private Type _monitorDataType = typeof(short);
|
||||||
|
|
||||||
public PlcDebugViewModel()
|
public PlcDebugViewModel(
|
||||||
|
IPlcService plcService,
|
||||||
|
ILogger<PlcDebugViewModel> logger,
|
||||||
|
IDispatcher dispatcher)
|
||||||
{
|
{
|
||||||
ConnectCommand = new RelayCommand(Connect);
|
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
|
||||||
DisconnectCommand = new RelayCommand(Disconnect);
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
ReadCommand = new RelayCommand(Read);
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
WriteCommand = new RelayCommand(Write);
|
|
||||||
ClearLogCommand = new RelayCommand(() => LogText = "");
|
// 初始化命令
|
||||||
|
ConnectCommand = new AsyncCommand(ConnectAsync);
|
||||||
|
DisconnectCommand = new AsyncCommand(DisconnectAsync);
|
||||||
|
ReadCommand = new AsyncCommand(ReadAsync);
|
||||||
|
WriteCommand = new AsyncCommand(WriteAsync);
|
||||||
|
AddMonitorCommand = new RelayCommand(_ => AddMonitor());
|
||||||
|
RemoveMonitorCommand = new RelayCommand<PlcTagViewModel>(RemoveMonitor, vm => vm != null);
|
||||||
|
AddToQueueCommand = new RelayCommand(_ => AddToQueue());
|
||||||
|
RemoveFromQueueCommand = new RelayCommand<WriteQueueItem>(RemoveFromQueue, item => item != null);
|
||||||
|
ExecuteQueueCommand = new AsyncCommand(ExecuteQueueAsync);
|
||||||
|
ClearQueueCommand = new RelayCommand(_ => WriteQueue.Clear());
|
||||||
|
MoveUpQueueCommand = new RelayCommand<WriteQueueItem>(MoveUpQueue, item => item != null);
|
||||||
|
MoveDownQueueCommand = new RelayCommand<WriteQueueItem>(MoveDownQueue, item => item != null);
|
||||||
|
SaveQueueCommand = new AsyncCommand(SaveQueueAsync);
|
||||||
|
LoadQueueCommand = new AsyncCommand(LoadQueueAsync);
|
||||||
|
|
||||||
|
AppendLog("PLC 调试工具已加载");
|
||||||
|
AppendLog("提示:需要配置真实的 PLC 连接信息才能使用");
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 属性
|
#region 属性
|
||||||
@@ -49,6 +90,12 @@ namespace Sharp.Demo.ViewModels.Windows
|
|||||||
set => SetProperty(ref _readValue, value);
|
set => SetProperty(ref _readValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Type ReadDataType
|
||||||
|
{
|
||||||
|
get => _readDataType;
|
||||||
|
set => SetProperty(ref _readDataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
public string WriteAddress
|
public string WriteAddress
|
||||||
{
|
{
|
||||||
get => _writeAddress;
|
get => _writeAddress;
|
||||||
@@ -61,57 +108,773 @@ namespace Sharp.Demo.ViewModels.Windows
|
|||||||
set => SetProperty(ref _writeValue, value);
|
set => SetProperty(ref _writeValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string LogText
|
public Type WriteDataType
|
||||||
{
|
{
|
||||||
get => _logText;
|
get => _writeDataType;
|
||||||
set => SetProperty(ref _logText, value);
|
set => SetProperty(ref _writeDataType, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string LastLogMessage
|
||||||
|
{
|
||||||
|
get => _lastLogMessage;
|
||||||
|
set => SetProperty(ref _lastLogMessage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string MonitorAddress
|
||||||
|
{
|
||||||
|
get => _monitorAddress;
|
||||||
|
set => SetProperty(ref _monitorAddress, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type MonitorDataType
|
||||||
|
{
|
||||||
|
get => _monitorDataType;
|
||||||
|
set => SetProperty(ref _monitorDataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支持的数据类型
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<DataTypeOption> SupportedDataTypes { get; } = new()
|
||||||
|
{
|
||||||
|
new("Bool", typeof(bool)),
|
||||||
|
new("Byte", typeof(byte)),
|
||||||
|
new("Short (Int16)", typeof(short)),
|
||||||
|
new("UShort (UInt16)", typeof(ushort)),
|
||||||
|
new("Int (Int32)", typeof(int)),
|
||||||
|
new("UInt (UInt32)", typeof(uint)),
|
||||||
|
new("Long (Int64)", typeof(long)),
|
||||||
|
new("ULong (UInt64)", typeof(ulong)),
|
||||||
|
new("Float (Real)", typeof(float)),
|
||||||
|
new("Double (LReal)", typeof(double)),
|
||||||
|
new("String", typeof(string))
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 监控列表
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入队列
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<WriteQueueItem> WriteQueue { get; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
|
|
||||||
public RelayCommand ConnectCommand { get; }
|
public ICommand ConnectCommand { get; }
|
||||||
public RelayCommand DisconnectCommand { get; }
|
public ICommand DisconnectCommand { get; }
|
||||||
public RelayCommand ReadCommand { get; }
|
public ICommand ReadCommand { get; }
|
||||||
public RelayCommand WriteCommand { get; }
|
public ICommand WriteCommand { get; }
|
||||||
public RelayCommand ClearLogCommand { get; }
|
public ICommand AddMonitorCommand { get; }
|
||||||
|
public ICommand RemoveMonitorCommand { get; }
|
||||||
|
public ICommand AddToQueueCommand { get; }
|
||||||
|
public ICommand RemoveFromQueueCommand { get; }
|
||||||
|
public ICommand ExecuteQueueCommand { get; }
|
||||||
|
public ICommand ClearQueueCommand { get; }
|
||||||
|
public ICommand MoveUpQueueCommand { get; }
|
||||||
|
public ICommand MoveDownQueueCommand { get; }
|
||||||
|
public ICommand SaveQueueCommand { get; }
|
||||||
|
public ICommand LoadQueueCommand { get; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 命令实现
|
#region 私有方法
|
||||||
|
|
||||||
private void Connect()
|
private async Task ConnectAsync()
|
||||||
{
|
{
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] 正在连接 PLC: {IpAddress}");
|
try
|
||||||
Status = "已连接";
|
{
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] ✅ 连接成功");
|
Status = "正在连接...";
|
||||||
|
AppendLog($"正在连接 PLC: {IpAddress}");
|
||||||
|
|
||||||
|
await _plcService.StartAsync();
|
||||||
|
|
||||||
|
Status = "已连接";
|
||||||
|
AppendLog("✅ PLC 连接成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Status = $"连接失败: {ex.Message}";
|
||||||
|
AppendLog($"❌ 连接失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "PLC 连接失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Disconnect()
|
private async Task DisconnectAsync()
|
||||||
{
|
{
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] 断开连接");
|
try
|
||||||
Status = "未连接";
|
{
|
||||||
|
await _plcService.StopAsync();
|
||||||
|
Status = "已断开";
|
||||||
|
AppendLog("PLC 已断开连接");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 断开失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "PLC 断开失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Read()
|
private async Task ReadAsync()
|
||||||
{
|
{
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] 读取 {ReadAddress}");
|
try
|
||||||
ReadValue = "42"; // 示例值
|
{
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] ✅ 读取成功: {ReadValue}");
|
AppendLog($"读取地址: {ReadAddress}, 类型: {GetTypeName(ReadDataType)}");
|
||||||
|
|
||||||
|
// 使用反射调用泛型方法
|
||||||
|
var method = _plcService.GetType().GetMethod(nameof(IPlcService.ReadAsync))!
|
||||||
|
.MakeGenericMethod(ReadDataType);
|
||||||
|
|
||||||
|
var task = (Task)method.Invoke(_plcService, new object[] { ReadAddress })!;
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 获取 Result 属性
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result")!;
|
||||||
|
var result = resultProperty.GetValue(task);
|
||||||
|
|
||||||
|
// 检查 IsSuccess
|
||||||
|
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
|
||||||
|
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
var valueProperty = result.GetType().GetProperty("Value")!;
|
||||||
|
var value = valueProperty.GetValue(result);
|
||||||
|
ReadValue = value?.ToString() ?? "null";
|
||||||
|
AppendLog($"✅ 读取成功: {ReadValue}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
|
||||||
|
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
|
||||||
|
ReadValue = "读取失败";
|
||||||
|
AppendLog($"❌ 读取失败: {errorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ReadValue = "读取失败";
|
||||||
|
AppendLog($"❌ 读取异常: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "PLC 读取失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Write()
|
private async Task WriteAsync()
|
||||||
{
|
{
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] 写入 {WriteAddress} = {WriteValue}");
|
try
|
||||||
AppendLog($"[{DateTime.Now:HH:mm:ss}] ✅ 写入成功");
|
{
|
||||||
}
|
// 解析值
|
||||||
|
var value = ParseValue(WriteValue, WriteDataType);
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 写入失败: 值格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
AppendLog($"写入地址: {WriteAddress} = {value}, 类型: {GetTypeName(WriteDataType)}");
|
||||||
|
|
||||||
|
// 使用反射调用泛型方法
|
||||||
|
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
|
||||||
|
var task = (Task)method.Invoke(_plcService, new object[] { WriteAddress, value })!;
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 获取 Result 属性
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result")!;
|
||||||
|
var result = resultProperty.GetValue(task);
|
||||||
|
|
||||||
|
// 检查 IsSuccess
|
||||||
|
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
|
||||||
|
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
AppendLog($"✅ 写入成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
|
||||||
|
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
|
||||||
|
AppendLog($"❌ 写入失败: {errorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 写入异常: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "PLC 写入失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AppendLog(string message)
|
private void AppendLog(string message)
|
||||||
{
|
{
|
||||||
LogText += message + "\n";
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
LastLogMessage = $"[{timestamp}] {message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加监控
|
||||||
|
/// </summary>
|
||||||
|
private void AddMonitor()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tag = new PlcTag
|
||||||
|
{
|
||||||
|
Address = MonitorAddress,
|
||||||
|
DataType = MonitorDataType
|
||||||
|
};
|
||||||
|
|
||||||
|
var vm = new PlcTagViewModel(tag);
|
||||||
|
|
||||||
|
tag.OnChanged = (t, v) =>
|
||||||
|
{
|
||||||
|
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
vm.UpdateValue();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_plcService.Subscribe(tag);
|
||||||
|
|
||||||
|
// 在UI线程添加到集合
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
MonitoredTags.Add(vm);
|
||||||
|
});
|
||||||
|
|
||||||
|
AppendLog($"✅ 添加监控: {MonitorAddress} ({GetTypeName(MonitorDataType)})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 添加监控失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "添加监控失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除监控
|
||||||
|
/// </summary>
|
||||||
|
private void RemoveMonitor(PlcTagViewModel? tagVm)
|
||||||
|
{
|
||||||
|
if (tagVm == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从PLC服务取消订阅
|
||||||
|
_plcService.Unsubscribe(tagVm.Id);
|
||||||
|
|
||||||
|
// 从UI集合移除
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
MonitoredTags.Remove(tagVm);
|
||||||
|
});
|
||||||
|
|
||||||
|
AppendLog($"✅ 移除监控: {tagVm.Address}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 移除监控失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "移除监控失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加到写入队列
|
||||||
|
/// </summary>
|
||||||
|
private void AddToQueue()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = ParseValue(WriteValue, WriteDataType);
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 添加到队列失败: 值格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new WriteQueueItem
|
||||||
|
{
|
||||||
|
Order = WriteQueue.Count + 1,
|
||||||
|
Address = WriteAddress,
|
||||||
|
Value = value,
|
||||||
|
DataType = WriteDataType,
|
||||||
|
DataTypeName = GetTypeName(WriteDataType)
|
||||||
|
};
|
||||||
|
|
||||||
|
WriteQueue.Add(item);
|
||||||
|
AppendLog($"✅ 添加到队列: {WriteAddress} = {value} ({GetTypeName(WriteDataType)})");
|
||||||
|
|
||||||
|
// 更新序号
|
||||||
|
UpdateQueueOrder();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 添加到队列失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "添加到队列失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从队列移除
|
||||||
|
/// </summary>
|
||||||
|
private void RemoveFromQueue(WriteQueueItem? item)
|
||||||
|
{
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
WriteQueue.Remove(item);
|
||||||
|
UpdateQueueOrder();
|
||||||
|
AppendLog($"移除队列项: {item.Address}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行写入队列
|
||||||
|
/// </summary>
|
||||||
|
private async Task ExecuteQueueAsync()
|
||||||
|
{
|
||||||
|
var enabledItems = WriteQueue.Where(x => x.Enabled).ToList();
|
||||||
|
|
||||||
|
if (enabledItems.Count == 0)
|
||||||
|
{
|
||||||
|
AppendLog("⚠️ 队列中没有启用的项,无需执行");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLog($"开始执行写入队列 ({enabledItems.Count}/{WriteQueue.Count} 项启用)...");
|
||||||
|
|
||||||
|
int successCount = 0;
|
||||||
|
int failCount = 0;
|
||||||
|
int verifyFailCount = 0;
|
||||||
|
|
||||||
|
foreach (var item in enabledItems)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppendLog($"[{item.Order}] 写入: {item.Address} = {item.Value} (类型: {item.DataTypeName})");
|
||||||
|
|
||||||
|
// 使用反射调用泛型方法
|
||||||
|
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
|
||||||
|
var task = (Task)method.Invoke(_plcService, new object[] { item.Address, item.Value })!;
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 获取 Result 属性
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result")!;
|
||||||
|
var result = resultProperty.GetValue(task);
|
||||||
|
|
||||||
|
// 检查 IsSuccess
|
||||||
|
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
|
||||||
|
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
|
||||||
|
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
AppendLog($" ✅ 写入成功");
|
||||||
|
|
||||||
|
// 校验
|
||||||
|
if (item.Verify)
|
||||||
|
{
|
||||||
|
AppendLog($" 🔍 开始校验...");
|
||||||
|
await Task.Delay(50); // 给PLC一点时间处理
|
||||||
|
|
||||||
|
var readMethod = _plcService.GetType().GetMethod(nameof(IPlcService.ReadAsync))!
|
||||||
|
.MakeGenericMethod(item.DataType);
|
||||||
|
var readTask = (Task)readMethod.Invoke(_plcService, new object[] { item.Address })!;
|
||||||
|
await readTask.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var readResultProperty = readTask.GetType().GetProperty("Result")!;
|
||||||
|
var readResult = readResultProperty.GetValue(readTask);
|
||||||
|
var readIsSuccess = (bool)readResult!.GetType().GetProperty("IsSuccess")!.GetValue(readResult)!;
|
||||||
|
|
||||||
|
if (readIsSuccess)
|
||||||
|
{
|
||||||
|
var readValue = readResult.GetType().GetProperty("Value")!.GetValue(readResult);
|
||||||
|
if (Equals(readValue, item.Value))
|
||||||
|
{
|
||||||
|
AppendLog($" ✅ 校验通过: {readValue}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
verifyFailCount++;
|
||||||
|
AppendLog($" ❌ 校验失败: 期望 {item.Value}, 实际 {readValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
verifyFailCount++;
|
||||||
|
var errorMsg = readResult.GetType().GetProperty("ErrorMessage")!.GetValue(readResult);
|
||||||
|
AppendLog($" ❌ 校验失败: 读取错误 - {errorMsg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
// 延迟
|
||||||
|
if (item.DelayMs > 0)
|
||||||
|
{
|
||||||
|
AppendLog($" ⏱️ 等待 {item.DelayMs} ms...");
|
||||||
|
await Task.Delay(item.DelayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failCount++;
|
||||||
|
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
|
||||||
|
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
|
||||||
|
AppendLog($" ❌ 写入失败: {errorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failCount++;
|
||||||
|
AppendLog($" ❌ 异常: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "写入队列项失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLog($"队列执行完成: 成功 {successCount}, 失败 {failCount}, 校验失败 {verifyFailCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列项上移
|
||||||
|
/// </summary>
|
||||||
|
private void MoveUpQueue(WriteQueueItem? item)
|
||||||
|
{
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
var index = WriteQueue.IndexOf(item);
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
WriteQueue.Move(index, index - 1);
|
||||||
|
UpdateQueueOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列项下移
|
||||||
|
/// </summary>
|
||||||
|
private void MoveDownQueue(WriteQueueItem? item)
|
||||||
|
{
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
var index = WriteQueue.IndexOf(item);
|
||||||
|
if (index < WriteQueue.Count - 1)
|
||||||
|
{
|
||||||
|
WriteQueue.Move(index, index + 1);
|
||||||
|
UpdateQueueOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新队列序号
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateQueueOrder()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < WriteQueue.Count; i++)
|
||||||
|
{
|
||||||
|
WriteQueue[i].Order = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析值
|
||||||
|
/// </summary>
|
||||||
|
private object? ParseValue(string valueStr, Type type)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (type == typeof(bool))
|
||||||
|
return bool.Parse(valueStr);
|
||||||
|
if (type == typeof(byte))
|
||||||
|
return byte.Parse(valueStr);
|
||||||
|
if (type == typeof(short))
|
||||||
|
return short.Parse(valueStr);
|
||||||
|
if (type == typeof(ushort))
|
||||||
|
return ushort.Parse(valueStr);
|
||||||
|
if (type == typeof(int))
|
||||||
|
return int.Parse(valueStr);
|
||||||
|
if (type == typeof(uint))
|
||||||
|
return uint.Parse(valueStr);
|
||||||
|
if (type == typeof(long))
|
||||||
|
return long.Parse(valueStr);
|
||||||
|
if (type == typeof(ulong))
|
||||||
|
return ulong.Parse(valueStr);
|
||||||
|
if (type == typeof(float))
|
||||||
|
return float.Parse(valueStr);
|
||||||
|
if (type == typeof(double))
|
||||||
|
return double.Parse(valueStr);
|
||||||
|
if (type == typeof(string))
|
||||||
|
return valueStr;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取类型名称
|
||||||
|
/// </summary>
|
||||||
|
private string GetTypeName(Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(bool)) return "Bool";
|
||||||
|
if (type == typeof(byte)) return "Byte";
|
||||||
|
if (type == typeof(short)) return "Short";
|
||||||
|
if (type == typeof(ushort)) return "UShort";
|
||||||
|
if (type == typeof(int)) return "Int";
|
||||||
|
if (type == typeof(uint)) return "UInt";
|
||||||
|
if (type == typeof(long)) return "Long";
|
||||||
|
if (type == typeof(ulong)) return "ULong";
|
||||||
|
if (type == typeof(float)) return "Float";
|
||||||
|
if (type == typeof(double)) return "Double";
|
||||||
|
if (type == typeof(string)) return "String";
|
||||||
|
return type.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存队列到JSON文件
|
||||||
|
/// </summary>
|
||||||
|
private async Task SaveQueueAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PlcQueue.json");
|
||||||
|
|
||||||
|
var queueData = WriteQueue.Select(item => new
|
||||||
|
{
|
||||||
|
item.Order,
|
||||||
|
item.Address,
|
||||||
|
Value = item.Value.ToString(),
|
||||||
|
DataTypeName = item.DataTypeName,
|
||||||
|
item.Enabled,
|
||||||
|
item.DelayMs,
|
||||||
|
item.Verify
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(queueData, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(filePath, json);
|
||||||
|
AppendLog($"✅ 队列已保存到: {filePath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 保存失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "保存队列失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从JSON文件加载队列
|
||||||
|
/// </summary>
|
||||||
|
private async Task LoadQueueAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PlcQueue.json");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
AppendLog("⚠️ 队列文件不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync(filePath);
|
||||||
|
var queueData = JsonSerializer.Deserialize<List<QueueItemData>>(json);
|
||||||
|
|
||||||
|
if (queueData == null || queueData.Count == 0)
|
||||||
|
{
|
||||||
|
AppendLog("⚠️ 队列文件为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteQueue.Clear();
|
||||||
|
|
||||||
|
foreach (var data in queueData)
|
||||||
|
{
|
||||||
|
// 根据类型名称获取 Type
|
||||||
|
Type? dataType = data.DataTypeName switch
|
||||||
|
{
|
||||||
|
"Bool" => typeof(bool),
|
||||||
|
"Byte" => typeof(byte),
|
||||||
|
"Short" => typeof(short),
|
||||||
|
"UShort" => typeof(ushort),
|
||||||
|
"Int" => typeof(int),
|
||||||
|
"UInt" => typeof(uint),
|
||||||
|
"Long" => typeof(long),
|
||||||
|
"ULong" => typeof(ulong),
|
||||||
|
"Float" => typeof(float),
|
||||||
|
"Double" => typeof(double),
|
||||||
|
"String" => typeof(string),
|
||||||
|
_ => typeof(short)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析值
|
||||||
|
var value = ParseValue(data.Value, dataType);
|
||||||
|
if (value == null) continue;
|
||||||
|
|
||||||
|
var item = new WriteQueueItem
|
||||||
|
{
|
||||||
|
Order = data.Order,
|
||||||
|
Address = data.Address,
|
||||||
|
Value = value,
|
||||||
|
DataType = dataType,
|
||||||
|
DataTypeName = data.DataTypeName,
|
||||||
|
Enabled = data.Enabled,
|
||||||
|
DelayMs = data.DelayMs,
|
||||||
|
Verify = data.Verify
|
||||||
|
};
|
||||||
|
|
||||||
|
WriteQueue.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateQueueOrder();
|
||||||
|
AppendLog($"✅ 已加载 {WriteQueue.Count} 个队列项");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"❌ 加载失败: {ex.Message}");
|
||||||
|
_logger.LogError(ex, "加载队列失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 队列项数据(用于JSON序列化)
|
||||||
|
/// </summary>
|
||||||
|
public class QueueItemData
|
||||||
|
{
|
||||||
|
public int Order { get; set; }
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public string DataTypeName { get; set; } = "Short";
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public int DelayMs { get; set; }
|
||||||
|
public bool Verify { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PLC 标签 ViewModel - 用于监控列表显示
|
||||||
|
/// </summary>
|
||||||
|
public class PlcTagViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly PlcTag _tag;
|
||||||
|
|
||||||
|
public PlcTagViewModel(PlcTag tag)
|
||||||
|
{
|
||||||
|
_tag = tag ?? throw new ArgumentNullException(nameof(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id => _tag.Id;
|
||||||
|
public string Address => _tag.Address;
|
||||||
|
public Type DataType => _tag.DataType;
|
||||||
|
public string DataTypeName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_tag.DataType == typeof(bool)) return "Bool";
|
||||||
|
if (_tag.DataType == typeof(byte)) return "Byte";
|
||||||
|
if (_tag.DataType == typeof(short)) return "Short";
|
||||||
|
if (_tag.DataType == typeof(ushort)) return "UShort";
|
||||||
|
if (_tag.DataType == typeof(int)) return "Int";
|
||||||
|
if (_tag.DataType == typeof(uint)) return "UInt";
|
||||||
|
if (_tag.DataType == typeof(long)) return "Long";
|
||||||
|
if (_tag.DataType == typeof(ulong)) return "ULong";
|
||||||
|
if (_tag.DataType == typeof(float)) return "Float";
|
||||||
|
if (_tag.DataType == typeof(double)) return "Double";
|
||||||
|
if (_tag.DataType == typeof(string)) return "String";
|
||||||
|
return _tag.DataType.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public object? Value => _tag.Value;
|
||||||
|
public PlcTag Tag => _tag;
|
||||||
|
|
||||||
|
public void UpdateValue()
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据类型选项
|
||||||
|
/// </summary>
|
||||||
|
public record DataTypeOption(string Name, Type Type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入队列项
|
||||||
|
/// </summary>
|
||||||
|
public class WriteQueueItem : ViewModelBase
|
||||||
|
{
|
||||||
|
private int _order;
|
||||||
|
private string _address = string.Empty;
|
||||||
|
private object _value = 0;
|
||||||
|
private Type _dataType = typeof(short);
|
||||||
|
private string _dataTypeName = "Short";
|
||||||
|
private bool _enabled = true;
|
||||||
|
private int _delayMs = 0;
|
||||||
|
private bool _verify = false;
|
||||||
|
|
||||||
|
public int Order
|
||||||
|
{
|
||||||
|
get => _order;
|
||||||
|
set => SetProperty(ref _order, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Address
|
||||||
|
{
|
||||||
|
get => _address;
|
||||||
|
set => SetProperty(ref _address, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Value
|
||||||
|
{
|
||||||
|
get => _value;
|
||||||
|
set => SetProperty(ref _value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type DataType
|
||||||
|
{
|
||||||
|
get => _dataType;
|
||||||
|
set => SetProperty(ref _dataType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DataTypeName
|
||||||
|
{
|
||||||
|
get => _dataTypeName;
|
||||||
|
set => SetProperty(ref _dataTypeName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用此项
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled
|
||||||
|
{
|
||||||
|
get => _enabled;
|
||||||
|
set => SetProperty(ref _enabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行后延迟(毫秒)
|
||||||
|
/// </summary>
|
||||||
|
public int DelayMs
|
||||||
|
{
|
||||||
|
get => _delayMs;
|
||||||
|
set => SetProperty(ref _delayMs, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否写入后校验
|
||||||
|
/// </summary>
|
||||||
|
public bool Verify
|
||||||
|
{
|
||||||
|
get => _verify;
|
||||||
|
set => SetProperty(ref _verify, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="600"
|
d:DesignHeight="1000"
|
||||||
d:DesignWidth="800">
|
d:DesignWidth="1200">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="20">
|
<StackPanel Margin="20">
|
||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
@@ -62,13 +62,14 @@
|
|||||||
Padding="20"
|
Padding="20"
|
||||||
Margin="0,0,0,15">
|
Margin="0,0,0,15">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="单次读写测试 (数据类型: Short)" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
|
<TextBlock Text="单次读写测试" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
|
||||||
|
|
||||||
<!-- 读取 -->
|
<!-- 读取 -->
|
||||||
<Grid Margin="0,0,0,15">
|
<Grid Margin="0,0,0,15">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="80"/>
|
<ColumnDefinition Width="80"/>
|
||||||
<ColumnDefinition Width="200"/>
|
<ColumnDefinition Width="200"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
<ColumnDefinition Width="100"/>
|
<ColumnDefinition Width="100"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
@@ -76,10 +77,15 @@
|
|||||||
<TextBlock Text="读取地址:" VerticalAlignment="Center"/>
|
<TextBlock Text="读取地址:" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Padding="8,0"
|
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Padding="8,0"
|
||||||
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
|
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
|
||||||
<Button Grid.Column="2" Content="读取" Height="32" Margin="0,0,10,0"
|
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
|
||||||
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
|
SelectedValue="{Binding ReadDataType}"
|
||||||
|
SelectedValuePath="Type"
|
||||||
|
DisplayMemberPath="Name"/>
|
||||||
|
<Button Grid.Column="3" Content="读取" Height="32" Margin="0,0,10,0"
|
||||||
Command="{Binding ReadCommand}"
|
Command="{Binding ReadCommand}"
|
||||||
Style="{StaticResource ButtonPrimary}"/>
|
Style="{StaticResource ButtonPrimary}"/>
|
||||||
<TextBlock Grid.Column="3" Text="{Binding ReadValue}" VerticalAlignment="Center"
|
<TextBlock Grid.Column="4" Text="{Binding ReadValue}" VerticalAlignment="Center"
|
||||||
FontWeight="Bold" FontSize="14"/>
|
FontWeight="Bold" FontSize="14"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -88,6 +94,8 @@
|
|||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="80"/>
|
<ColumnDefinition Width="80"/>
|
||||||
<ColumnDefinition Width="200"/>
|
<ColumnDefinition Width="200"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
<ColumnDefinition Width="100"/>
|
||||||
<ColumnDefinition Width="100"/>
|
<ColumnDefinition Width="100"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
@@ -95,15 +103,165 @@
|
|||||||
<TextBlock Text="写入地址:" VerticalAlignment="Center"/>
|
<TextBlock Text="写入地址:" VerticalAlignment="Center"/>
|
||||||
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Padding="8,0"
|
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Padding="8,0"
|
||||||
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
|
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
|
||||||
<TextBox Grid.Column="2" Text="{Binding WriteValue}" Height="32" Padding="8,0"
|
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
|
||||||
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
|
SelectedValue="{Binding WriteDataType}"
|
||||||
|
SelectedValuePath="Type"
|
||||||
|
DisplayMemberPath="Name"/>
|
||||||
|
<TextBox Grid.Column="3" Text="{Binding WriteValue}" Height="32" Padding="8,0"
|
||||||
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
|
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
|
||||||
<Button Grid.Column="3" Content="写入" Width="100" Height="32" HorizontalAlignment="Left"
|
<Button Grid.Column="4" Content="写入" Height="32" Margin="0,0,10,0"
|
||||||
Command="{Binding WriteCommand}"
|
Command="{Binding WriteCommand}"
|
||||||
Style="{StaticResource ButtonWarning}"/>
|
Style="{StaticResource ButtonWarning}"/>
|
||||||
|
<Button Grid.Column="5" Content="添加到队列" Width="100" Height="32" HorizontalAlignment="Left"
|
||||||
|
Command="{Binding AddToQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonInfo}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- 两列布局:监控列表 和 写入队列 -->
|
||||||
|
<Grid Margin="0,0,0,15">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="15"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 监控列表 -->
|
||||||
|
<Border
|
||||||
|
Grid.Column="0"
|
||||||
|
Background="{DynamicResource RegionBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="300"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="实时监控列表" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<!-- 添加监控 -->
|
||||||
|
<Grid Grid.Row="1" Margin="0,0,0,10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
<ColumnDefinition Width="100"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBox Text="{Binding MonitorAddress}" Height="32" Padding="8,0"
|
||||||
|
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||||
|
hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)"/>
|
||||||
|
<ComboBox Grid.Column="1" Height="32" Margin="0,0,10,0"
|
||||||
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
|
SelectedValue="{Binding MonitorDataType}"
|
||||||
|
SelectedValuePath="Type"
|
||||||
|
DisplayMemberPath="Name"/>
|
||||||
|
<Button Grid.Column="2" Content="添加监控" Height="32"
|
||||||
|
Command="{Binding AddMonitorCommand}"
|
||||||
|
Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 监控列表 -->
|
||||||
|
<DataGrid Grid.Row="2" ItemsSource="{Binding MonitoredTags}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
IsReadOnly="True"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
CanUserResizeRows="False"
|
||||||
|
CanUserSortColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="150"/>
|
||||||
|
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="*"/>
|
||||||
|
<DataGridTemplateColumn Header="操作" Width="80">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Content="移除" Height="28" Padding="10,0"
|
||||||
|
Command="{Binding DataContext.RemoveMonitorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDanger}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 写入队列 -->
|
||||||
|
<Border
|
||||||
|
Grid.Column="2"
|
||||||
|
Background="{DynamicResource RegionBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="300"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||||
|
<TextBlock Text="写入队列" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding WriteQueue.Count, StringFormat=({0} 项)}"
|
||||||
|
VerticalAlignment="Center" Margin="10,0,0,0"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 队列列表 -->
|
||||||
|
<DataGrid Grid.Row="1" ItemsSource="{Binding WriteQueue}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
IsReadOnly="True"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
CanUserResizeRows="False"
|
||||||
|
CanUserSortColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="#" Binding="{Binding Order}" Width="40"/>
|
||||||
|
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="120"/>
|
||||||
|
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="70"/>
|
||||||
|
<DataGridTextColumn Header="值" Binding="{Binding Value}" Width="80"/>
|
||||||
|
<DataGridTemplateColumn Header="操作" Width="*">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Button Content="↑" Width="30" Height="28" Margin="2"
|
||||||
|
Command="{Binding DataContext.MoveUpQueueCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDefault}"/>
|
||||||
|
<Button Content="↓" Width="30" Height="28" Margin="2"
|
||||||
|
Command="{Binding DataContext.MoveDownQueueCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDefault}"/>
|
||||||
|
<Button Content="×" Width="30" Height="28" Margin="2"
|
||||||
|
Command="{Binding DataContext.RemoveFromQueueCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDanger}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<!-- 队列操作按钮 -->
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,10,0,0" HorizontalAlignment="Right">
|
||||||
|
<Button Content="执行队列" Width="100" Height="35" Margin="0,0,10,0"
|
||||||
|
Command="{Binding ExecuteQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
<Button Content="清空队列" Width="100" Height="35"
|
||||||
|
Command="{Binding ClearQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonWarning}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- 日志区域 -->
|
<!-- 日志区域 -->
|
||||||
<Border
|
<Border
|
||||||
Background="{DynamicResource RegionBrush}"
|
Background="{DynamicResource RegionBrush}"
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="PLC 调试工具"
|
Title="PLC 调试工具"
|
||||||
Height="650"
|
Height="800"
|
||||||
Width="900"
|
Width="1200"
|
||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -18,29 +19,42 @@
|
|||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<Border Grid.Row="0" Background="{DynamicResource RegionBrush}"
|
<Border Grid.Row="0" Background="{DynamicResource RegionBrush}"
|
||||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1" Padding="15,10">
|
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1" Padding="15,10">
|
||||||
<StackPanel Orientation="Horizontal">
|
<Grid>
|
||||||
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
<Grid.RowDefinitions>
|
||||||
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0"/>
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Button Content="🔗 连接" Width="90" Height="32" Margin="0,0,10,0"
|
<StackPanel Orientation="Horizontal">
|
||||||
Command="{Binding ConnectCommand}"/>
|
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
<Button Content="🔌 断开" Width="90" Height="32" Margin="0,0,10,0"
|
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0"/>
|
||||||
Command="{Binding DisconnectCommand}"/>
|
|
||||||
|
|
||||||
<Separator Margin="10,0"/>
|
<Button Content="🔗 连接" Width="90" Height="32" Margin="0,0,10,0"
|
||||||
|
Command="{Binding ConnectCommand}" Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
<Button Content="🔌 断开" Width="90" Height="32" Margin="0,0,10,0"
|
||||||
|
Command="{Binding DisconnectCommand}" Style="{StaticResource ButtonDanger}"/>
|
||||||
|
|
||||||
<TextBlock Text="状态:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
<Separator Margin="10,0"/>
|
||||||
<TextBlock Text="{Binding Status}" FontWeight="Bold" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource SuccessBrush}"/>
|
<TextBlock Text="状态:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
</StackPanel>
|
<TextBlock Text="{Binding Status}" FontWeight="Bold" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource SuccessBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 最新日志 -->
|
||||||
|
<TextBlock Grid.Row="1" Text="{Binding LastLogMessage}"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}"
|
||||||
|
FontSize="11" Margin="0,5,0,0" TextTrimming="CharacterEllipsis"/>
|
||||||
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<TabControl Grid.Row="1" Margin="15">
|
<TabControl Grid.Row="1" Margin="10">
|
||||||
<!-- 单次读写 -->
|
<!-- 单次读写 -->
|
||||||
<TabItem Header="📝 单次读写">
|
<TabItem Header="📝 单次读写">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="15">
|
<StackPanel Margin="15">
|
||||||
|
<!-- 读取 -->
|
||||||
<Border Background="{DynamicResource RegionBrush}"
|
<Border Background="{DynamicResource RegionBrush}"
|
||||||
BorderBrush="{DynamicResource BorderBrush}"
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
BorderThickness="1" CornerRadius="8" Padding="20" Margin="0,0,0,15">
|
BorderThickness="1" CornerRadius="8" Padding="20" Margin="0,0,0,15">
|
||||||
@@ -50,20 +64,27 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="150"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
<TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Margin="0,0,15,0"/>
|
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Margin="0,0,10,0"/>
|
||||||
<Button Grid.Column="2" Content="读取" Width="100" Height="32" Margin="0,0,15,0"
|
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
|
||||||
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
|
SelectedValue="{Binding ReadDataType}"
|
||||||
|
SelectedValuePath="Type"
|
||||||
|
DisplayMemberPath="Name"/>
|
||||||
|
<Button Grid.Column="3" Content="读取" Width="100" Height="32" Margin="0,0,10,0"
|
||||||
Command="{Binding ReadCommand}" Style="{StaticResource ButtonPrimary}"/>
|
Command="{Binding ReadCommand}" Style="{StaticResource ButtonPrimary}"/>
|
||||||
<TextBlock Grid.Column="3" Text="{Binding ReadValue}" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="4" Text="{Binding ReadValue}" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- 写入 -->
|
||||||
<Border Background="{DynamicResource RegionBrush}"
|
<Border Background="{DynamicResource RegionBrush}"
|
||||||
BorderBrush="{DynamicResource BorderBrush}"
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
BorderThickness="1" CornerRadius="8" Padding="20">
|
BorderThickness="1" CornerRadius="8" Padding="20">
|
||||||
@@ -77,17 +98,26 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="250"/>
|
||||||
|
<ColumnDefinition Width="150"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
<TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Margin="0,0,0,10"/>
|
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Margin="0,0,10,0"/>
|
||||||
|
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
|
||||||
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
|
SelectedValue="{Binding WriteDataType}"
|
||||||
|
SelectedValuePath="Type"
|
||||||
|
DisplayMemberPath="Name"/>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Text="值:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
<TextBlock Grid.Row="1" Text="值:" VerticalAlignment="Center" Margin="0,10,10,0"/>
|
||||||
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal">
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding WriteValue}" Height="32" Margin="0,10,10,0"/>
|
||||||
<TextBox Text="{Binding WriteValue}" Width="150" Height="32" Margin="0,0,10,0"/>
|
<StackPanel Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
<Button Content="写入" Width="100" Height="32"
|
<Button Content="写入" Width="100" Height="32" Margin="0,0,10,0"
|
||||||
Command="{Binding WriteCommand}"/>
|
Command="{Binding WriteCommand}" Style="{StaticResource ButtonWarning}"/>
|
||||||
|
<Button Content="添加到队列" Width="120" Height="32"
|
||||||
|
Command="{Binding AddToQueueCommand}" Style="{StaticResource ButtonInfo}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -96,34 +126,186 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<!-- 日志 -->
|
<!-- 实时监控 -->
|
||||||
<TabItem Header="📋 操作日志">
|
<TabItem Header="📊 实时监控">
|
||||||
<Border Margin="15" Background="{DynamicResource RegionBrush}"
|
<Grid Margin="15">
|
||||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="8">
|
<Grid.RowDefinitions>
|
||||||
<Grid Margin="15">
|
<RowDefinition Height="Auto"/>
|
||||||
<Grid.RowDefinitions>
|
<RowDefinition Height="*"/>
|
||||||
<RowDefinition Height="Auto"/>
|
</Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
<!-- 添加监控 -->
|
||||||
<TextBlock Text="操作日志" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
|
<Border Background="{DynamicResource RegionBrush}"
|
||||||
<Button Content="清空" Width="80" Height="30" Margin="15,0,0,0"
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
Command="{Binding ClearLogCommand}"/>
|
BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,0,0,15">
|
||||||
</StackPanel>
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="150"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
<TextBlock Text="添加监控:" VerticalAlignment="Center" Margin="0,0,10,0" FontWeight="Bold"/>
|
||||||
<TextBox Text="{Binding LogText, Mode=OneWay}"
|
<TextBox Grid.Column="1" Text="{Binding MonitorAddress}" Height="32" Margin="0,0,10,0"
|
||||||
IsReadOnly="True"
|
hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)"/>
|
||||||
TextWrapping="Wrap"
|
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
|
||||||
Background="{DynamicResource SecondaryRegionBrush}"
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
BorderThickness="0"
|
SelectedValue="{Binding MonitorDataType}"
|
||||||
FontFamily="Consolas"
|
SelectedValuePath="Type"
|
||||||
FontSize="11"
|
DisplayMemberPath="Name"/>
|
||||||
Padding="10"/>
|
<Button Grid.Column="3" Content="添加监控" Width="100" Height="32"
|
||||||
</ScrollViewer>
|
Command="{Binding AddMonitorCommand}" Style="{StaticResource ButtonSuccess}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- 监控列表 -->
|
||||||
|
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="8" Padding="15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="监控列表" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<DataGrid Grid.Row="1" ItemsSource="{Binding MonitoredTags}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
IsReadOnly="True"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
CanUserResizeRows="False"
|
||||||
|
CanUserSortColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="200"/>
|
||||||
|
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="*"/>
|
||||||
|
<DataGridTemplateColumn Header="操作" Width="100">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button Content="移除" Height="28" Padding="10,0"
|
||||||
|
Command="{Binding DataContext.RemoveMonitorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDanger}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- 写入队列 -->
|
||||||
|
<TabItem Header="📋 写入队列">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 添加队列项 -->
|
||||||
|
<Border Background="{DynamicResource RegionBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,0,0,15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="150"/>
|
||||||
|
<ColumnDefinition Width="150"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="添加项目:" VerticalAlignment="Center" Margin="0,0,10,0" FontWeight="Bold"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Margin="0,0,10,0"
|
||||||
|
hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)"/>
|
||||||
|
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
|
||||||
|
ItemsSource="{Binding SupportedDataTypes}"
|
||||||
|
SelectedValue="{Binding WriteDataType}"
|
||||||
|
SelectedValuePath="Type"
|
||||||
|
DisplayMemberPath="Name"/>
|
||||||
|
<TextBox Grid.Column="3" Text="{Binding WriteValue}" Height="32" Margin="0,0,10,0"
|
||||||
|
hc:InfoElement.Placeholder="值"/>
|
||||||
|
<Button Grid.Column="4" Content="添加到队列" Width="120" Height="32"
|
||||||
|
Command="{Binding AddToQueueCommand}" Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 队列列表 -->
|
||||||
|
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}"
|
||||||
|
BorderBrush="{DynamicResource BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,0,0,15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||||
|
<TextBlock Text="队列列表" FontSize="14" FontWeight="Bold" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="{Binding WriteQueue.Count, StringFormat=({0} 项)}"
|
||||||
|
VerticalAlignment="Center" Margin="10,0,0,0"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<DataGrid Grid.Row="1" ItemsSource="{Binding WriteQueue}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
CanUserResizeRows="False"
|
||||||
|
CanUserSortColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="#" Binding="{Binding Order}" Width="40" IsReadOnly="True"/>
|
||||||
|
<DataGridCheckBoxColumn Header="启用" Binding="{Binding Enabled}" Width="50"/>
|
||||||
|
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="140" IsReadOnly="True"/>
|
||||||
|
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="80" IsReadOnly="True"/>
|
||||||
|
<DataGridTextColumn Header="值" Binding="{Binding Value}" Width="80" IsReadOnly="True"/>
|
||||||
|
<DataGridTextColumn Header="延迟(ms)" Binding="{Binding DelayMs}" Width="80"/>
|
||||||
|
<DataGridCheckBoxColumn Header="校验" Binding="{Binding Verify}" Width="50"/>
|
||||||
|
<DataGridTemplateColumn Header="操作" Width="*">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
|
<Button Content="↑" Width="30" Height="28" Margin="2"
|
||||||
|
Command="{Binding DataContext.MoveUpQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDefault}"/>
|
||||||
|
<Button Content="↓" Width="30" Height="28" Margin="2"
|
||||||
|
Command="{Binding DataContext.MoveDownQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDefault}"/>
|
||||||
|
<Button Content="×" Width="30" Height="28" Margin="2"
|
||||||
|
Command="{Binding DataContext.RemoveFromQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
CommandParameter="{Binding}"
|
||||||
|
Style="{StaticResource ButtonDanger}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 队列操作按钮 -->
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="保存队列" Width="100" Height="38" Margin="0,0,10,0"
|
||||||
|
Command="{Binding SaveQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonInfo}"/>
|
||||||
|
<Button Content="加载队列" Width="100" Height="38" Margin="0,0,10,0"
|
||||||
|
Command="{Binding LoadQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonInfo}"/>
|
||||||
|
<Button Content="执行队列" Width="100" Height="38" Margin="0,0,10,0"
|
||||||
|
Command="{Binding ExecuteQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
<Button Content="清空队列" Width="100" Height="38"
|
||||||
|
Command="{Binding ClearQueueCommand}"
|
||||||
|
Style="{StaticResource ButtonWarning}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
Reference in new issue
Block a user