Compare commits
25
Commits
66bb6d36ba
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac26ab255c | ||
|
|
60d788178f | ||
|
|
fa76bdb61c | ||
|
|
e7702405d8 | ||
|
|
2ddaf0e6f7 | ||
|
|
bd1c25a67d | ||
|
|
9c7c426507 | ||
|
|
11fe48ea9a | ||
|
|
18995ee0e2 | ||
|
|
d83843d2fd | ||
|
|
d88b81a8f0 | ||
|
|
d35e451c40 | ||
|
|
3b7a41d7f8 | ||
|
|
e8a7aa52aa | ||
|
|
c41190aa33 | ||
|
|
f46df16275 | ||
|
|
eabe73edd5 | ||
|
|
e144dc78a7 | ||
|
|
2a2c50257e | ||
|
|
99ac3f2b8d | ||
|
|
80a2cbd80b | ||
|
|
57db6776b8 | ||
|
|
062582e238 | ||
|
|
deb3544143 | ||
|
|
5a565f7361 |
@@ -1,4 +1,5 @@
|
||||
using Common;
|
||||
using Command;
|
||||
using ACP.ViewModels;
|
||||
using ACP.ViewModels.Dialogs;
|
||||
using ACP.Views;
|
||||
@@ -12,6 +13,8 @@ using Service.Interface;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using UIShare.PubEvent;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
@@ -23,6 +26,7 @@ using DeviceCommand.Base;
|
||||
using AutoMapper;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ACP.Profiles;
|
||||
using Prism.Dialogs;
|
||||
using UIShare.Helpers;
|
||||
|
||||
namespace ACP
|
||||
@@ -60,6 +64,56 @@ namespace ACP
|
||||
}
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// 向命令库 CommandDialog 注入弹窗能力(命令库是纯 .NET 类库,不引用 WPF,这里通过委托实现依赖倒置)
|
||||
var dialogService = Container.Resolve<IDialogService>();
|
||||
CommandDialog.弹窗处理器 = (弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct) =>
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
CancellationTokenRegistration registration = default;
|
||||
if (是否阻塞)
|
||||
{
|
||||
registration = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||
}
|
||||
// 命令可能在后台线程执行,统一切回 UI 线程弹窗
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var param = new DialogParameters
|
||||
{
|
||||
{ "Title", 弹窗类型 switch
|
||||
{
|
||||
CommandDialog.DialogType.Warning => "警告",
|
||||
CommandDialog.DialogType.Error => "错误",
|
||||
_ => "信息提示"
|
||||
} },
|
||||
{ "Message", 弹窗详细 ?? string.Empty },
|
||||
{ "Icon", 弹窗类型 switch
|
||||
{
|
||||
CommandDialog.DialogType.Warning => "warn",
|
||||
CommandDialog.DialogType.Error => "error",
|
||||
_ => "info"
|
||||
} },
|
||||
{ "ShowOk", true },
|
||||
{ "AutoCloseSeconds", (double)自动关闭秒数 }
|
||||
};
|
||||
if (是否阻塞)
|
||||
{
|
||||
// 阻塞:用户手动关闭或自动关闭后才继续执行步骤
|
||||
dialogService.ShowDialog("MessageBox", param, _ =>
|
||||
{
|
||||
registration.Dispose();
|
||||
tcs.TrySetResult(true);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非阻塞:弹出后步骤立即继续(不等关闭回调)
|
||||
dialogService.Show("MessageBox", param, _ => { });
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
});
|
||||
return tcs.Task;
|
||||
};
|
||||
|
||||
// 配置全局日志分发器:按 CurrentScope 路由到对应 LogArea
|
||||
var globalInfo = Container.Resolve<GlobalInfo>();
|
||||
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
||||
@@ -67,6 +121,8 @@ namespace ACP
|
||||
//初始化数据库
|
||||
DatabaseConfig.SetTenant(10001);
|
||||
DatabaseConfig.InitSqlite();
|
||||
// 启动时检查数据库文件是否过期,过期则归档(重命名加时间戳),后续会自动创建新库
|
||||
DatabaseConfig.TryArchiveOldDatabase();
|
||||
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
||||
SqlSugarContext.InitDatabase();
|
||||
//显示登录窗口
|
||||
@@ -126,6 +182,7 @@ namespace ACP
|
||||
//注册服务
|
||||
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
|
||||
containerRegistry.Register<ITestReportService, TestReportService>();
|
||||
containerRegistry.Register<ITestCheckRecordService, TestCheckRecordService>();
|
||||
}
|
||||
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
||||
protected override IModuleCatalog CreateModuleCatalog()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.ViewModelBase;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace ACP.ViewModels.Dialogs
|
||||
{
|
||||
@@ -69,6 +71,11 @@ namespace ACP.ViewModels.Dialogs
|
||||
|
||||
public DialogCloseListener RequestClose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动关闭定时器:自动关闭秒数大于 0 时启动,到时自动关闭弹窗(同时解除阻塞等待)
|
||||
/// </summary>
|
||||
private DispatcherTimer _autoCloseTimer;
|
||||
|
||||
public MessageBoxViewModel(IContainerProvider containerProvider):base(containerProvider)
|
||||
{
|
||||
YesCommand = new DelegateCommand(OnYes);
|
||||
@@ -93,6 +100,7 @@ namespace ACP.ViewModels.Dialogs
|
||||
|
||||
public override void OnDialogClosed()
|
||||
{
|
||||
_autoCloseTimer?.Stop();
|
||||
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
|
||||
}
|
||||
|
||||
@@ -117,6 +125,21 @@ namespace ACP.ViewModels.Dialogs
|
||||
ShowNo = parameters.GetValue<bool>("ShowNo");
|
||||
ShowOk = parameters.GetValue<bool>("ShowOk");
|
||||
ShowCancel = parameters.GetValue<bool>("ShowCancel");
|
||||
|
||||
// 自动关闭:秒数大于 0 时启动定时器,到时自动关闭(供命令库 弹窗 命令的自动关闭参数使用)
|
||||
if (parameters.TryGetValue("AutoCloseSeconds", out double autoCloseSeconds) && autoCloseSeconds > 0)
|
||||
{
|
||||
_autoCloseTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(autoCloseSeconds)
|
||||
};
|
||||
_autoCloseTimer.Tick += (s, e) =>
|
||||
{
|
||||
_autoCloseTimer.Stop();
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
};
|
||||
_autoCloseTimer.Start();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
using SqlSugar;
|
||||
using TSMasterCAN;
|
||||
|
||||
namespace ACP.ViewModels
|
||||
{
|
||||
@@ -43,6 +44,23 @@ namespace ACP.ViewModels
|
||||
private readonly DispatcherTimer _uiRefreshTimer;
|
||||
#endregion
|
||||
|
||||
#region 子程序导航
|
||||
|
||||
public bool CanGoBack =>
|
||||
(CurrentContext?.MainNav.CanGoBack ?? false) ||
|
||||
(CurrentContext?.ErrorNav.CanGoBack ?? false);
|
||||
|
||||
private void OnGoBack()
|
||||
{
|
||||
var ctx = CurrentContext;
|
||||
if (ctx == null) return;
|
||||
// 工具栏按钮同时重置两个 Tab 的导航
|
||||
ctx.MainNav.Reset(ctx.Program, "主程序");
|
||||
ctx.ErrorNav.Reset(ctx.Program, "错误程序");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性
|
||||
public string Title
|
||||
{
|
||||
@@ -155,6 +173,8 @@ namespace ACP.ViewModels
|
||||
public ICommand MonitorValueSettingCommand { get; set; }
|
||||
public ICommand GetFileStringCommand { get; set; }
|
||||
public ICommand SilenceBuzzerCommand { get; set; }
|
||||
public ICommand SettingChannelCommand { get; set; }
|
||||
public ICommand GoBackCommand { get; set; }
|
||||
#endregion
|
||||
|
||||
public ShellViewModel(IContainerProvider containerProvider)
|
||||
@@ -204,8 +224,10 @@ namespace ACP.ViewModels
|
||||
MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
|
||||
GetFileStringCommand = new DelegateCommand(GetFileString);
|
||||
SilenceBuzzerCommand = new AsyncDelegateCommand(SilenceBuzzer);
|
||||
|
||||
_globalInfo.ContextDic.Add("default", new ScopedContext());
|
||||
SettingChannelCommand = new AsyncDelegateCommand(SilenceBuzzer);
|
||||
SettingChannelCommand = new DelegateCommand(SettingChannel);
|
||||
GoBackCommand = new DelegateCommand(OnGoBack);
|
||||
_globalInfo.ContextDic.TryAdd("default", new ScopedContext());
|
||||
|
||||
_eventAggregator.GetEvent<LoginSuccessEvent>().Subscribe(() =>
|
||||
{
|
||||
@@ -234,10 +256,20 @@ namespace ACP.ViewModels
|
||||
RaisePropertyChanged(nameof(RunIcon));
|
||||
RaisePropertyChanged(nameof(RunningTime));
|
||||
RaisePropertyChanged(nameof(IsTerminate));
|
||||
RaisePropertyChanged(nameof(CanGoBack));
|
||||
}
|
||||
|
||||
#region 命令处理与事件
|
||||
|
||||
private void SettingChannel()
|
||||
{
|
||||
var re = CAN.ShowChannelMappingWindow(true);
|
||||
if (re != 0)
|
||||
{
|
||||
var msg = CAN.GetErrorDescription(re);
|
||||
if(CurrentConfig!=null)
|
||||
LoggerHelper.ErrorWithNotify(CurrentConfig.Title,$"同星通道映射界面打开失败:{msg}");
|
||||
}
|
||||
}
|
||||
private async Task SilenceBuzzer()
|
||||
{
|
||||
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Publish(_globalInfo.CurrentScope);
|
||||
@@ -507,6 +539,9 @@ namespace ACP.ViewModels
|
||||
targetContext.IsTerminate = false;
|
||||
}
|
||||
|
||||
// 清空子程序导航栈,回到根程序
|
||||
targetContext.ResetNavigation();
|
||||
|
||||
if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
|
||||
}
|
||||
|
||||
@@ -589,6 +624,7 @@ namespace ACP.ViewModels
|
||||
targetContext.Program.Parameters.Clear();
|
||||
targetContext.Program.StepCollection.Clear();
|
||||
targetContext.Program.ErrorStepCollection.Clear();
|
||||
targetContext.ResetNavigation();
|
||||
foreach (var item in CurrentConfig.ParameterList)
|
||||
{
|
||||
var param = CurrentConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == item.Name);
|
||||
@@ -653,7 +689,7 @@ namespace ACP.ViewModels
|
||||
targetContext.Program.StepCollection = program.StepCollection;
|
||||
targetContext.Program.ErrorStepCollection = program.ErrorStepCollection;
|
||||
targetContext.CurrentFilePath = filePath;
|
||||
|
||||
targetContext.ResetNavigation();
|
||||
foreach (var item in CurrentConfig.SharedParameterList)
|
||||
{
|
||||
var parameter = targetContext?.Program?.Parameters?.FirstOrDefault(x => x.Name == item.ParameterName);
|
||||
|
||||
+23
-17
@@ -190,15 +190,19 @@
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<!-- CAN信号监控 -->
|
||||
<MenuItem Header="CAN信号监控"
|
||||
Foreground="Black"
|
||||
Command="{Binding SelectCANSignalMonitorCommand}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="Message"
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
<MenuItem Header="同星CAN"
|
||||
Foreground="Black">
|
||||
<MenuItem Header="通道映射"
|
||||
Command="{Binding SettingChannelCommand}"
|
||||
Foreground="Black" />
|
||||
<MenuItem Header="CAN信号采集设置"
|
||||
Command="{Binding SelectCANSignalMonitorCommand}"
|
||||
Foreground="Black" />
|
||||
<MenuItem Header="CAN报文字符串获取"
|
||||
Command="{Binding SelectCANMessageCommand}"
|
||||
Foreground="Black" />
|
||||
</MenuItem>
|
||||
|
||||
<!-- 监控设置 -->
|
||||
<MenuItem Header="监控设置"
|
||||
Foreground="Black"
|
||||
@@ -208,15 +212,7 @@ Command="{Binding MonitorValueSettingCommand}">
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="CAN报文选择"
|
||||
Foreground="Black"
|
||||
Command="{Binding SelectCANMessageCommand}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="Message"
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
|
||||
|
||||
<!-- 获得文件路径字符串 -->
|
||||
<MenuItem Header="获得文件路径字符串"
|
||||
Foreground="Black"
|
||||
@@ -248,6 +244,16 @@ Command="{Binding GetFileStringCommand}">
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<!-- 返回上一级程序 -->
|
||||
<MenuItem Header="返回上一级程序"
|
||||
Foreground="Black"
|
||||
Command="{Binding GoBackCommand}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="ArrowLeftBold"
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem Header="切换初始主界面"
|
||||
FontSize="13"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using CANModule.ViewModels;
|
||||
using CANModule.Views;
|
||||
|
||||
namespace CANModule
|
||||
@@ -12,7 +13,9 @@ namespace CANModule
|
||||
{
|
||||
containerRegistry.RegisterDialog<SelectCanMessageView>("SelectCanMessageView");
|
||||
containerRegistry.RegisterDialog<CollectCANSignalSettingView>("CollectCANSignalSettingView");
|
||||
containerRegistry.RegisterDialog<DBCInfoView>("DBCInfo");
|
||||
containerRegistry.RegisterForNavigation<CANView>("CANView");
|
||||
containerRegistry.Register<CANViewModel>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ using System.Windows.Threading;
|
||||
using TSMasterCAN;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.UIViewModel;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace CANModule.ViewModels
|
||||
{
|
||||
public class CANViewModel : BindableBase, IDialogAware
|
||||
public class CANViewModel : NavigateViewModelBase
|
||||
{
|
||||
|
||||
#region 属性
|
||||
@@ -269,17 +270,15 @@ namespace CANModule.ViewModels
|
||||
|
||||
#endregion
|
||||
private IEventAggregator _eventAggregator { get; set; }
|
||||
private CAN? _canfd;
|
||||
private SystemConfig? _systemConfig;
|
||||
private readonly DeviceManager _deviceManager;
|
||||
private IDialogService _dialogService { get; set; }
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
public CANViewModel(IContainerProvider containerProvider)
|
||||
public CANViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
|
||||
_systemConfig = containerProvider.Resolve<SystemConfig>();
|
||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||
_canfd = _deviceManager.CANFD;
|
||||
_dialogService = containerProvider.Resolve<IDialogService>();
|
||||
SelectBLFPathCommand = new DelegateCommand(SelectBLFPath);
|
||||
SaveBLFPathCommand = new DelegateCommand(SaveBLFPath);
|
||||
@@ -299,6 +298,7 @@ namespace CANModule.ViewModels
|
||||
LoadMessageCommand = new DelegateCommand(LoadMessage);
|
||||
DeleteDcAutoLoadCommand = new DelegateCommand(DeleteDcAutoLoad);
|
||||
SendCustomMessageCommand = new DelegateCommand(SendCustomMessage);
|
||||
Task.Run(() => Refresh(cts.Token), cts.Token);
|
||||
}
|
||||
|
||||
#region 命令
|
||||
@@ -496,7 +496,7 @@ namespace CANModule.ViewModels
|
||||
{
|
||||
while (ct.IsCancellationRequested == false)
|
||||
{
|
||||
var temp = CAN.RealTimeMessages.Select(s => s.Value).ToArray();
|
||||
var temp = _deviceManager._CANMonitoringService.RealTimeMessages.Select(s => s.Value).ToArray();
|
||||
foreach (var item in temp)
|
||||
{
|
||||
var find = CanMessageList.FirstOrDefault(s => s.报文ID == item.FIdentifier);
|
||||
@@ -571,6 +571,10 @@ namespace CANModule.ViewModels
|
||||
}
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
|
||||
}
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
Task.Run(() => Refresh(cts.Token), cts.Token);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Configuration;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Model;
|
||||
using Model.Models;
|
||||
using TSMasterCAN;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace CANModule.ViewModels
|
||||
{
|
||||
public class DBCInfoViewModel :BindableBase, IDialogAware
|
||||
{
|
||||
private can_network _can_Network;
|
||||
|
||||
public required can_network can_Network
|
||||
{
|
||||
get { return _can_Network; }
|
||||
set { SetProperty(ref _can_Network, value); }
|
||||
}
|
||||
private List<_Msg_> _MessageInfo;
|
||||
|
||||
public required List<_Msg_> MessageInfo
|
||||
{
|
||||
get { return _MessageInfo; }
|
||||
set { SetProperty(ref _MessageInfo, value); }
|
||||
}
|
||||
private ObservableCollection<InstructionNode> _CanNetWorkNode = new();
|
||||
public ObservableCollection<InstructionNode> CanNetWorkNode
|
||||
{
|
||||
get => _CanNetWorkNode;
|
||||
set => SetProperty(ref _CanNetWorkNode, value);
|
||||
}
|
||||
private ObservableCollection<InstructionNode> _MessgaeNode = new();
|
||||
public ObservableCollection<InstructionNode> MessgaeNode
|
||||
{
|
||||
get => _MessgaeNode;
|
||||
set => SetProperty(ref _MessgaeNode, value);
|
||||
}
|
||||
public ICommand CloseCommand { get; set; }
|
||||
public DialogCloseListener RequestClose { get; set; }
|
||||
public DBCInfoViewModel()
|
||||
{
|
||||
CloseCommand = new DelegateCommand(Close);
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
RequestClose.Invoke();
|
||||
}
|
||||
|
||||
public bool CanCloseDialog()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnDialogClosed()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
if (parameters.ContainsKey("can_Network"))
|
||||
{
|
||||
can_Network = parameters.GetValue<can_network>("can_Network");
|
||||
}
|
||||
|
||||
if (parameters.ContainsKey("MsgDatabase"))
|
||||
{
|
||||
MessageInfo = parameters.GetValue<List<_Msg_>>("MsgDatabase");
|
||||
}
|
||||
InitTreeNode();
|
||||
}
|
||||
|
||||
private void InitTreeNode()
|
||||
{
|
||||
foreach(var item in can_Network.can_nodes)
|
||||
{
|
||||
var tx = new ObservableCollection<InstructionNode>();
|
||||
var rx = new ObservableCollection<InstructionNode>();
|
||||
foreach (var can_Message in item.rx_can_Messages)
|
||||
{
|
||||
rx.Add(new InstructionNode
|
||||
{
|
||||
Name = can_Message.message_name
|
||||
});
|
||||
}
|
||||
foreach (var can_Message in item.tx_can_Messages)
|
||||
{
|
||||
tx.Add(new InstructionNode
|
||||
{
|
||||
Name = can_Message.message_name
|
||||
});
|
||||
}
|
||||
var children = new ObservableCollection<InstructionNode>();
|
||||
children.Add(new InstructionNode
|
||||
{
|
||||
Name = "TX",
|
||||
Children = tx
|
||||
});
|
||||
children.Add(new InstructionNode
|
||||
{
|
||||
Name = "RX",
|
||||
Children = rx
|
||||
});
|
||||
CanNetWorkNode.Add(new InstructionNode
|
||||
{
|
||||
Name = item.node_name,
|
||||
Children = children
|
||||
});
|
||||
}
|
||||
foreach(var item in MessageInfo)
|
||||
{
|
||||
var children = new ObservableCollection<InstructionNode>();
|
||||
foreach(var Signalname in item.signal_Name)
|
||||
{
|
||||
children.Add(new InstructionNode
|
||||
{
|
||||
Name = Signalname
|
||||
});
|
||||
}
|
||||
string hexValue = "[0x" + item.msg_id.ToString("X") + "]";
|
||||
MessgaeNode.Add(new InstructionNode
|
||||
{
|
||||
Name = hexValue+item.msg_name,
|
||||
Children=children
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,12 @@
|
||||
Foreground="White"
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0,10,0" />
|
||||
<Button Content="关闭"
|
||||
<!--<Button Content="关闭"
|
||||
Grid.Column="2"
|
||||
Margin="150 0 0 0"
|
||||
Width="60"
|
||||
Command="{Binding CloseCommand}"
|
||||
Height="25" />
|
||||
Height="25" />-->
|
||||
</Grid>
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<UserControl x:Class="CANModule.Views.DBCInfoView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:oxy="http://oxyplot.org/wpf"
|
||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:cons="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
Background="White"
|
||||
xmlns:model="clr-namespace:Model.Models;assembly=Model"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
Height="400"
|
||||
Width="800">
|
||||
<prism:Dialog.WindowStyle>
|
||||
<Style BasedOn="{StaticResource DialogUserManageStyle}"
|
||||
TargetType="Window" />
|
||||
</prism:Dialog.WindowStyle>
|
||||
<GroupBox MouseLeftButtonDown="MouseLeftButtonDown">
|
||||
<GroupBox.Header>
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="CAN报文查看"
|
||||
|
||||
Foreground="White"
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0,10,0" />
|
||||
<Button Content="关闭"
|
||||
Grid.Column="2"
|
||||
Margin="150 0 0 0"
|
||||
Width="60"
|
||||
Command="{Binding CloseCommand}"
|
||||
Height="25"
|
||||
/>
|
||||
</Grid>
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="5" />
|
||||
<!-- Splitter 列 -->
|
||||
<ColumnDefinition Width="2*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TreeView ItemsSource="{Binding MessgaeNode}">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type model:InstructionNode}"
|
||||
ItemsSource="{Binding Children}">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
</TreeView>
|
||||
<GridSplitter Grid.Column="1"
|
||||
Width="5"
|
||||
Background="Gray"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
ShowsPreview="True" />
|
||||
<TreeView Grid.Column="2"
|
||||
ItemsSource="{Binding CanNetWorkNode}">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type model:InstructionNode}"
|
||||
ItemsSource="{Binding Children}">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace CANModule.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// DBCInfoView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class DBCInfoView : UserControl
|
||||
{
|
||||
public DBCInfoView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
Window.GetWindow(this)?.DragMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Common.Attributes;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Command
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话框命令:在测试流程中弹出提示窗口。
|
||||
/// 命令库为纯 .NET 类库,不直接引用 WPF 程序集;
|
||||
/// 实际的弹窗能力由宿主程序(ACP 外壳)在启动时注入到 <see cref="弹窗处理器"/> 委托中实现(依赖倒置)。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public static class CommandDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// 弹窗类型
|
||||
/// </summary>
|
||||
public enum DialogType
|
||||
{
|
||||
/// <summary>
|
||||
/// 信息提示
|
||||
/// </summary>
|
||||
Info,
|
||||
/// <summary>
|
||||
/// 警告提示
|
||||
/// </summary>
|
||||
Warning,
|
||||
/// <summary>
|
||||
/// 错误提示
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗处理委托(参数依次为:弹窗类型、弹窗详细、是否阻塞、自动关闭秒数、取消令牌)。
|
||||
/// 由宿主程序启动时注入实现;未注入时弹窗命令降级为仅输出日志,不会抛异常。
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public static Func<DialogType, string, bool, float, CancellationToken, Task> 弹窗处理器;
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗:在界面上显示一个提示对话框。
|
||||
/// </summary>
|
||||
/// <param name="弹窗类型">弹窗样式:Info 信息 / Warning 警告 / Error 错误</param>
|
||||
/// <param name="弹窗详细">弹窗中显示的详细内容</param>
|
||||
/// <param name="是否阻塞">true = 步骤暂停,等待用户关闭(或自动关闭)后才继续;false = 弹出后步骤立即继续</param>
|
||||
/// <param name="自动关闭秒数">大于 0 时,弹窗在指定秒数后自动关闭;小于等于 0 时不自动关闭,需用户手动关闭</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 弹窗(DialogType 弹窗类型, string 弹窗详细, bool 是否阻塞, float 自动关闭秒数, CancellationToken ct)
|
||||
{
|
||||
var handler = 弹窗处理器;
|
||||
if (handler == null)
|
||||
{
|
||||
Console.WriteLine($"[弹窗命令] 宿主未注入弹窗处理器,跳过弹窗:{弹窗详细}");
|
||||
return;
|
||||
}
|
||||
await handler(弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -23,7 +24,7 @@ namespace Common.Tools
|
||||
{
|
||||
foreach (var kvp in processedVariables)
|
||||
{
|
||||
expr.Parameters[kvp.Key] = kvp.Value;
|
||||
expr.Parameters[kvp.Key] = NormalizeValue(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +96,29 @@ namespace Common.Tools
|
||||
return (processedExpression, newVariables);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归一化变量取值:输入变量的值常以字符串形式保存(如 "10"),
|
||||
/// 若直接参与比较,NCalc 会按字符串逐位比较("10" > "5" 为假),
|
||||
/// 导致大于/小于判断失真。此处将可解析为数字/布尔的字符串转换为对应类型,
|
||||
/// 使比较按数值语义进行;无法解析的字符串保持原样。
|
||||
/// </summary>
|
||||
private static object? NormalizeValue(object? value)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)
|
||||
|| double.TryParse(s, out d))
|
||||
{
|
||||
return d;
|
||||
}
|
||||
if (bool.TryParse(s, out var b))
|
||||
{
|
||||
return b;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 检查字符串是否包含中文字符
|
||||
private static bool ContainsChinese(string text)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,21 +2,358 @@
|
||||
using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Devices
|
||||
{
|
||||
/// <summary>
|
||||
/// 水冷机 一对一
|
||||
/// ANEVH 操作模式枚举
|
||||
/// </summary>
|
||||
public enum ANEVH操作模式_枚举
|
||||
{
|
||||
/// <summary>定值模式</summary>
|
||||
FIXED,
|
||||
/// <summary>序列测试模式</summary>
|
||||
LIST
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ANEVH 系列双向可编程直流电源(一拖三),基于 TCP 通信,使用 SCPI 指令集。
|
||||
/// 作为高压源载一体机使用:同时具备直流电源输出(SOURce)与电子负载(SINK)能力。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class ANEVH80 : Tcp
|
||||
{
|
||||
// SCPI 指令默认使用 \n 作为结束符
|
||||
private const string ScpiDelimiter = "\n";
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,初始化 TCP 连接配置。
|
||||
/// </summary>
|
||||
/// <param name="config">TCP 连接配置(IP、端口等)</param>
|
||||
public ANEVH80(TcpConfig config) : base(config)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||
/// 支持科学计数法(如 2.48E-03),结果保留两位小数。
|
||||
/// </summary>
|
||||
/// <param name="raw">设备返回的原始字符串</param>
|
||||
/// <returns>提取到的数值(保留两位小数),解析失败时返回 0</returns>
|
||||
private static double ExtractDouble(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||
|
||||
// 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头
|
||||
var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||
? Math.Round(val, 2)
|
||||
: 0;
|
||||
}
|
||||
|
||||
#region 1. 系统与公共指令
|
||||
|
||||
/// <summary>
|
||||
/// 清除错误队列 (*CLS)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 清除错误队列Async(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重设仪器为出厂默认状态 (*RST)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 复位Async(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询仪器识别码 (*IDN?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>设备标识字符串(厂商,型号,序列号,固件版本)</returns>
|
||||
public virtual async Task<string> 查询设备信息Async(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询错误信息 (SYST:ERR?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>错误信息字符串</returns>
|
||||
public virtual async Task<string> 查询错误信息Async(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($"SYST:ERR?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 2. 输出控制 (OUTPut 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 设置输出开关状态 (OUTP ON / OFF)
|
||||
/// </summary>
|
||||
/// <param name="isOn">true: 开启输出, false: 关闭输出</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置输出开关Async(bool isOn, CancellationToken ct = default)
|
||||
{
|
||||
string state = isOn ? "ON" : "OFF";
|
||||
await SendAsync($"OUTP {state}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置操作模式 (OUTP:MODE)
|
||||
/// </summary>
|
||||
/// <param name="mode">操作模式枚举 (FIXED: 定值 | LIST: 序列测试)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置操作模式Async(ANEVH操作模式_枚举 mode, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"OUTP:MODE {mode}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 3. 源模式参数设置 (SOURce 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 设置源输出电压 (SOUR:VOLT)
|
||||
/// </summary>
|
||||
/// <param name="voltage">电压值 (单位: V)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置电压Async(double voltage, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = voltage.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:VOLT {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询源设定电压 (SOUR:VOLT?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>设定电压值 (单位: V)</returns>
|
||||
public virtual async Task<double> 查询设定电压Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"SOUR:VOLT?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置源正向限流 (SOUR:CURR)
|
||||
/// </summary>
|
||||
/// <param name="current">电流值 (单位: A,正值)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置电流Async(double current, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = current.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:CURR {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询源设定电流 (SOUR:CURR?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>设定电流值 (单位: A)</returns>
|
||||
public virtual async Task<double> 查询设定电流Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"SOUR:CURR?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置源正向限功率 (SOUR:POW)
|
||||
/// </summary>
|
||||
/// <param name="power">功率值 (单位: W,正值)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置功率Async(double power, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = power.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:POW {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询源设定功率 (SOUR:POW?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>设定功率值 (单位: W)</returns>
|
||||
public virtual async Task<double> 查询设定功率Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"SOUR:POW?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电压上升斜率 (SOUR:VOLT:SLEW:POS)
|
||||
/// </summary>
|
||||
/// <param name="slewRate">电压上升速率 (单位: V/ms)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置电压上升斜率Async(double slewRate, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = slewRate.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:VOLT:SLEW:POS {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电压下降斜率 (SOUR:VOLT:SLEW:NEG)
|
||||
/// </summary>
|
||||
/// <param name="slewRate">电压下降速率 (单位: V/ms)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置电压下降斜率Async(double slewRate, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = slewRate.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:VOLT:SLEW:NEG {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电流上升斜率 (SOUR:CURR:SLEW:POS)
|
||||
/// </summary>
|
||||
/// <param name="slewRate">电流上升速率 (单位: A/ms)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置电流上升斜率Async(double slewRate, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = slewRate.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:CURR:SLEW:POS {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电流下降斜率 (SOUR:CURR:SLEW:NEG)
|
||||
/// </summary>
|
||||
/// <param name="slewRate">电流下降速率 (单位: A/ms)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置电流下降斜率Async(double slewRate, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = slewRate.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:CURR:SLEW:NEG {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置过压保护值 (SOUR:VOLT:PROT)
|
||||
/// </summary>
|
||||
/// <param name="voltage">过压保护阈值 (单位: V)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置过压保护Async(double voltage, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = voltage.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:VOLT:PROT {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置过流保护值 (SOUR:CURR:PROT)
|
||||
/// </summary>
|
||||
/// <param name="current">过流保护阈值 (单位: A)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置过流保护Async(double current, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = current.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:CURR:PROT {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置过功率保护值 (SOUR:POW:PROT)
|
||||
/// </summary>
|
||||
/// <param name="power">过功率保护阈值 (单位: W)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置过功率保护Async(double power, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = power.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SOUR:POW:PROT {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 4. 负载模式参数设置 (SINK 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 设置负载吸收电流 (SINK:CURR)
|
||||
/// </summary>
|
||||
/// <param name="current">负载电流值 (单位: A)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置负载电流Async(double current, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = current.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SINK:CURR {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询负载设定电流 (SINK:CURR?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>负载电流设定值 (单位: A)</returns>
|
||||
public virtual async Task<double> 查询负载电流Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"SINK:CURR?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置负载吸收功率 (SINK:POW)
|
||||
/// </summary>
|
||||
/// <param name="power">负载功率值 (单位: W)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置负载功率Async(double power, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = power.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"SINK:POWer {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询负载设定功率 (SINK:POW?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>负载功率设定值 (单位: W)</returns>
|
||||
public virtual async Task<double> 查询负载功率Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"SINK:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 5. 测量查询 (MEASure 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 查询测量电压 (MEAS:VOLT?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>实际输出电压 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 读取电压Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:VOLT?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询测量电流 (MEAS:CURR?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>实际输出电流 (单位: A,保留两位小数,正值为源模式,负值为负载模式)</returns>
|
||||
public virtual async Task<double> 读取电流Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:CURR?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询测量功率 (MEAS:POW?)
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>实际输出功率 (单位: W,保留两位小数)</returns>
|
||||
public virtual async Task<double> 读取功率Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:POW?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Devices
|
||||
{
|
||||
/// <summary>
|
||||
/// 交流源 一拖三
|
||||
/// 交流源 一拖三 (Chroma 61800系列)
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class Chroma61800 : Tcp
|
||||
@@ -23,185 +22,372 @@ namespace DeviceCommand.Devices
|
||||
{
|
||||
}
|
||||
|
||||
#region 1. 基础系统控制
|
||||
/// <summary>
|
||||
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||
/// 支持科学计数法(如 2.48E-03),结果保留两位小数。
|
||||
/// </summary>
|
||||
/// <param name="raw">设备返回的原始字符串</param>
|
||||
/// <returns>提取到的数值(保留两位小数),解析失败时返回 0</returns>
|
||||
private static double ExtractDouble(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||
|
||||
public virtual async Task 清除错误队列(CancellationToken ct = default)
|
||||
// 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头
|
||||
var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||
? Math.Round(val, 2)
|
||||
: 0;
|
||||
}
|
||||
|
||||
#region 1. 基础系统与公共指令
|
||||
|
||||
/// <summary>
|
||||
/// 清除错误队列 (*CLS)
|
||||
/// </summary>
|
||||
public virtual async Task 清除错误队列Async(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 2. LIST 模式初始化
|
||||
/// <summary>
|
||||
/// 重设仪器为初始状态 (*RST)
|
||||
/// </summary>
|
||||
public virtual async Task 复位Async(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 LIST 模式的三相独立编辑环境
|
||||
/// 查询仪器识别码 (*IDN?)
|
||||
/// </summary>
|
||||
/// <param name="loopCount">循环次数,0代表无限循环</param>
|
||||
public virtual async Task 初始化三相List模式(int loopCount = 0, CancellationToken ct = default)
|
||||
public virtual async Task<string> 查询设备信息Async(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"INST:PHAS THRE{ScpiDelimiter}", ct); // 选择三相模式
|
||||
await SendAsync($"INST:EDIT EACH{ScpiDelimiter}", ct); // 选择编辑电压方式为分别编辑
|
||||
await SendAsync($"LIST:COUN {loopCount}{ScpiDelimiter}", ct); // 循环次数设定
|
||||
await SendAsync($"LIST:TRIG AUTO{ScpiDelimiter}", ct); // 设定触发方式
|
||||
await SendAsync($"LIST:BASE TIME{ScpiDelimiter}", ct); // 选择执行时间类别
|
||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 3. LIST 步阶参数配置 (独立发送版 - 三参数固定版)
|
||||
#region 2. 基础输出与耦合控制 (OUTPut 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 切换目标相位并设置波形为正弦波
|
||||
/// 建议在发送具体 LIST 参数前调用一次即可
|
||||
/// 设置输出开关状态 (OUTP ON / OFF)
|
||||
/// </summary>
|
||||
/// <param name="phase">相位 (1: L1, 2: L2, 3: L3)</param>
|
||||
public virtual async Task 切换相位Async(int phase, CancellationToken ct = default)
|
||||
/// <param name="isOn">true: 开启输出, false: 关闭输出</param>
|
||||
public virtual async Task 设置输出开关Async(bool isOn, CancellationToken ct = default)
|
||||
{
|
||||
if (phase < 1 || phase > 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(phase), "相位必须为 1, 2, 或 3");
|
||||
|
||||
await SendAsync($"INST:NSEL {phase}{ScpiDelimiter}", ct);
|
||||
await SendAsync($"FUNC:SHAP:A SINE{ScpiDelimiter}", ct);
|
||||
string state = isOn ? "ON" : "OFF";
|
||||
await SendAsync($"OUTP {state}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步起始角度
|
||||
/// 设置输出耦合模式 (OUTP:COUP AC | DC | ACDC)
|
||||
/// </summary>
|
||||
public virtual async Task 配置List起始角度Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
public virtual async Task 设置耦合模式Async(string couplingMode, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:DEGR {strValues}{ScpiDelimiter}", ct);
|
||||
// 有效参数: AC, DC, ACDC
|
||||
await SendAsync($"OUTP:COUP {couplingMode}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步交流起始电压
|
||||
/// 设置输出继电器状态 (OUTP:REL ON | OFF)
|
||||
/// </summary>
|
||||
public virtual async Task 配置List交流起始电压Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
public virtual async Task 设置输出继电器Async(bool isOn, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:VOLT:AC:STAR {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步交流结束电压
|
||||
/// </summary>
|
||||
public virtual async Task 配置List交流结束电压Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:VOLT:AC:END {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步直流起始电压
|
||||
/// </summary>
|
||||
public virtual async Task 配置List直流起始电压Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:VOLT:DC:STAR {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步直流结束电压
|
||||
/// </summary>
|
||||
public virtual async Task 配置List直流结束电压Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:VOLT:DC:END {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步起始频率
|
||||
/// </summary>
|
||||
public virtual async Task 配置List起始频率Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:FREQ:STAR {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步结束频率
|
||||
/// </summary>
|
||||
public virtual async Task 配置List结束频率Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:FREQ:END {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置 LIST 各步执行时间 (单位: 毫秒)
|
||||
/// </summary>
|
||||
public virtual async Task 配置List执行时间Async(double step1, double step2, double step3, CancellationToken ct = default)
|
||||
{
|
||||
string strValues = JoinParameters(step1, step2, step3);
|
||||
await SendAsync($"LIST:DWEL {strValues}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 辅助方法:将三个 double 值转换为逗号分隔的字符串,确保小数点格式正确
|
||||
/// </summary>
|
||||
private string JoinParameters(double val1, double val2, double val3)
|
||||
{
|
||||
return $"{val1.ToString("0.###", CultureInfo.InvariantCulture)}," +
|
||||
$"{val2.ToString("0.###", CultureInfo.InvariantCulture)}," +
|
||||
$"{val3.ToString("0.###", CultureInfo.InvariantCulture)}";
|
||||
string state = isOn ? "ON" : "OFF";
|
||||
await SendAsync($"OUTP:REL {state}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 4. LIST 触发与状态监控
|
||||
#region 3. 电源核心参数设置 (SOURCE 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 切换到 LIST 模式并触发输出
|
||||
/// 设置交流电压 (VOLT:AC)
|
||||
/// </summary>
|
||||
public virtual async Task 启动List输出(CancellationToken ct = default)
|
||||
/// <param name="voltage">电压值 (0 ~ 300V 或高压选配范围)</param>
|
||||
public virtual async Task 设置交流电压Async(double voltage, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"OUTP:MODE LIST{ScpiDelimiter}", ct);
|
||||
await SendAsync($"TRIG ON{ScpiDelimiter}", ct);
|
||||
string valStr = voltage.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"VOLT:AC {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 LIST 序列是否正在运行
|
||||
/// 设置直流电压 (VOLT:DC)
|
||||
/// </summary>
|
||||
public virtual async Task<bool> 查询List是否运行中(CancellationToken ct = default)
|
||||
public virtual async Task 设置直流电压Async(double voltage, CancellationToken ct = default)
|
||||
{
|
||||
string state = await WriteReadAsync($"TRIG:STATE?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
state = state?.Trim().ToUpper();
|
||||
return state == "ON" || state == "1";
|
||||
string valStr = voltage.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"VOLT:DC {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阻塞异步等待直到 LIST 序列执行完毕 (带安全超时保护)
|
||||
/// 设置输出频率 (FREQ)
|
||||
/// </summary>
|
||||
/// <param name="timeoutMs">最大等待超时时间(建议比实际LIST总时长多5-10秒)</param>
|
||||
public virtual async Task 等待List执行结束(int timeoutMs = 60000, CancellationToken ct = default)
|
||||
/// <param name="frequency">频率值 (Hz)</param>
|
||||
public virtual async Task 设置频率Async(double frequency, CancellationToken ct = default)
|
||||
{
|
||||
// 给仪器留出响应触发的时间
|
||||
await Task.Delay(500, ct);
|
||||
string valStr = frequency.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"FREQ {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
bool isRunning = true;
|
||||
|
||||
while (isRunning)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (sw.ElapsedMilliseconds > timeoutMs)
|
||||
{
|
||||
throw new TimeoutException($"等待 LIST 执行结束超时 ({timeoutMs} ms)。");
|
||||
}
|
||||
|
||||
// 读取当前状态
|
||||
isRunning = await 查询List是否运行中(ct);
|
||||
|
||||
if (isRunning)
|
||||
{
|
||||
// 还在运行,适当休眠后继续轮询,避免总线阻塞
|
||||
await Task.Delay(500, ct);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 设置波形选择 (FUNC:SHAP:A SINE / SQUA / CSIN 等)
|
||||
/// </summary>
|
||||
public virtual async Task 设置波形Async(string shape, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"FUNC:SHAP:A {shape}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 4. 相位与多相模式控制 (INST & PHASE 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 设置单/三相工作模式 (INST:PHAS THREE / SINGLE)
|
||||
/// </summary>
|
||||
/// <param name="isThreePhase">true: 三相模式, false: 单相模式</param>
|
||||
public virtual async Task 设置单三相模式Async(bool isThreePhase, CancellationToken ct = default)
|
||||
{
|
||||
string mode = isThreePhase ? "THREE" : "SINGLE";
|
||||
await SendAsync($"INST:PHAS {mode}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置三相模式下的相互关系 (PHASE:THREE INDEPEND / SAMEFREQ / BALANCE)
|
||||
/// </summary>
|
||||
public virtual async Task 设置三相关系Async(string relation, CancellationToken ct = default)
|
||||
{
|
||||
// 有效参数: INDEPEND, SAMEFREQ, BALANCE
|
||||
await SendAsync($"PHASE:THREE {relation}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置输出开启相位角 (PHASE:ON)
|
||||
/// </summary>
|
||||
public virtual async Task 设置开机角度Async(double degree, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = degree.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"PHASE:ON {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置输出关闭相位角 (PHASE:OFF)
|
||||
/// </summary>
|
||||
public virtual async Task 设置关机角度Async(double degree, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = degree.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
await SendAsync($"PHASE:OFF {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 5. 参数测量查询 (MEASure 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 查询交流电压均方根值 (MEAS:VOLT:AC?)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>交流电压均方根值 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 读取交流电压Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:VOLT:AC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询交流电流均方根值 (MEAS:CURR:AC?)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>交流电流均方根值 (单位: A,保留两位小数)</returns>
|
||||
public virtual async Task<double> 读取交流电流Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:CURR:AC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询输出频率 (MEAS:FREQ?)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>输出频率值 (单位: Hz,保留两位小数)</returns>
|
||||
public virtual async Task<double> 读取频率Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:FREQ?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询真实输出功率 (MEAS:POW:AC?)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>真实输出功率值 (单位: W,保留两位小数)</returns>
|
||||
public virtual async Task<double> 读取真实功率Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"MEAS:POW:AC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region 6. LIST 模式子系统 (LIST 子系统)
|
||||
|
||||
/// <summary>
|
||||
/// 设定列表功能的模态 (SOUR:LIST:COUP)
|
||||
/// </summary>
|
||||
/// <param name="coupling">耦合模态: ALL | NONE</param>
|
||||
public virtual async Task 设置列表耦合模态Async(string coupling, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"SOUR:LIST:COUP {coupling}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定列表功能的触发形态 (SOUR:LIST:TRIG)
|
||||
/// </summary>
|
||||
/// <param name="triggerMode">触发形态: AUTO | MANUAL | EXCITE</param>
|
||||
public virtual async Task 设置列表触发形态Async(string triggerMode, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"SOUR:LIST:TRIG {triggerMode}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询列表功能的有效序列数 (SOUR:LIST:POIN?)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>有效序列数 (0 ~ 100)</returns>
|
||||
public virtual async Task<int> 查询列表有效序列数Async(CancellationToken ct = default)
|
||||
{
|
||||
string resp = await WriteReadAsync($"SOUR:LIST:POIN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return int.TryParse(resp.Trim(), out int val) ? val : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定列表执行完成前的执行次数 (SOUR:LIST:COUN)
|
||||
/// </summary>
|
||||
/// <param name="count">执行次数 (0 ~ 65535)</param>
|
||||
public virtual async Task 设置列表执行次数Async(int count, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"SOUR:LIST:COUN {count}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定列表点的静止时间顺序 (SOUR:LIST:DWEL)
|
||||
/// </summary>
|
||||
/// <param name="dwellTimes">静止时间序列 (单位: ms,每个值 0 ~ 99999999.9)</param>
|
||||
public virtual async Task 设置列表静止时间顺序Async(double[] dwellTimes, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(dwellTimes, v => v.ToString("0.#", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:DWEL {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定波形缓冲区列表点数的顺序 (SOUR:LIST:SHAP)
|
||||
/// </summary>
|
||||
/// <param name="shapes">波形缓冲区顺序,每个元素为 A 或 B</param>
|
||||
public virtual async Task 设置列表波形顺序Async(string[] shapes, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", shapes);
|
||||
await SendAsync($"SOUR:LIST:SHAP {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定列表的时间基础 (SOUR:LIST:BASE)
|
||||
/// </summary>
|
||||
/// <param name="timeBase">时间基础: TIME | CYCLE</param>
|
||||
public virtual async Task 设置列表时间基础Async(string timeBase, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"SOUR:LIST:BASE {timeBase}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定 AC 起始电压列表点数的顺序 (SOUR:LIST:VOLT:AC:STAR)
|
||||
/// </summary>
|
||||
/// <param name="voltages">AC 起始电压序列 (单位: V,每个值 0.0 ~ 300.0)</param>
|
||||
public virtual async Task 设置列表AC起始电压顺序Async(double[] voltages, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(voltages, v => v.ToString("0.###", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:VOLT:AC:STAR {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定 AC 结束电压列表点数的顺序 (SOUR:LIST:VOLT:AC:END)
|
||||
/// </summary>
|
||||
/// <param name="voltages">AC 结束电压序列 (单位: V,每个值 0.0 ~ 300.0)</param>
|
||||
public virtual async Task 设置列表AC结束电压顺序Async(double[] voltages, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(voltages, v => v.ToString("0.###", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:VOLT:AC:END {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定 DC 起始电压列表点数的顺序 (SOUR:LIST:VOLT:DC:STAR)
|
||||
/// </summary>
|
||||
/// <param name="voltages">DC 起始电压序列 (单位: V,每个值 -424.2 ~ 414.2)</param>
|
||||
public virtual async Task 设置列表DC起始电压顺序Async(double[] voltages, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(voltages, v => v.ToString("0.###", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:VOLT:DC:STAR {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定 DC 结束电压列表点数的顺序 (SOUR:LIST:VOLT:DC:END)
|
||||
/// </summary>
|
||||
/// <param name="voltages">DC 结束电压序列 (单位: V,每个值 -424.2 ~ 414.2)</param>
|
||||
public virtual async Task 设置列表DC结束电压顺序Async(double[] voltages, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(voltages, v => v.ToString("0.###", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:VOLT:DC:END {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定起始频率列表点数的顺序 (SOUR:LIST:FREQ:STAR)
|
||||
/// </summary>
|
||||
/// <param name="frequencies">起始频率序列 (单位: Hz,每个值 15.00 ~ 100.00)</param>
|
||||
public virtual async Task 设置列表起始频率顺序Async(double[] frequencies, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(frequencies, v => v.ToString("0.##", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:FREQ:STAR {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定结束频率列表点数的顺序 (SOUR:LIST:FREQ:END)
|
||||
/// </summary>
|
||||
/// <param name="frequencies">结束频率序列 (单位: Hz,每个值 15.00 ~ 100.00)</param>
|
||||
public virtual async Task 设置列表结束频率顺序Async(double[] frequencies, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(frequencies, v => v.ToString("0.##", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:FREQ:END {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定相位角度列表点数的顺序 (SOUR:LIST:DEGR)
|
||||
/// </summary>
|
||||
/// <param name="degrees">相位角度序列 (单位: °,每个值 0.0 ~ 359.9)</param>
|
||||
public virtual async Task 设置列表相位角度顺序Async(double[] degrees, CancellationToken ct = default)
|
||||
{
|
||||
string valStr = string.Join(",", Array.ConvertAll(degrees, v => v.ToString("0.#", CultureInfo.InvariantCulture)));
|
||||
await SendAsync($"SOUR:LIST:DEGR {valStr}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设定操作模态 (OUTP:MODE)
|
||||
/// </summary>
|
||||
/// <param name="mode">操作模态: FIXED | LIST | PULSE | STEP | SYNTH | INTERHAR</param>
|
||||
public virtual async Task 设置操作模态Async(string mode, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"OUTP:MODE {mode}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 LIST 模态下设定执行状态 (TRIG)
|
||||
/// <para>需先通过 <see cref="设置操作模态Async"/> 将 OUTP:MODE 设为 LIST</para>
|
||||
/// </summary>
|
||||
/// <param name="isOn">true: 开始执行 LIST, false: 停止 LIST</param>
|
||||
public virtual async Task 触发列表执行Async(bool isOn, CancellationToken ct = default)
|
||||
{
|
||||
string state = isOn ? "ON" : "OFF";
|
||||
await SendAsync($"TRIG {state}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -45,33 +46,74 @@ namespace DeviceCommand.Devices
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 信号发生器 (DG1000Z 系列) 一拖三,支持波形输出与频率计测量
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class DG1000Z : Tcp
|
||||
{
|
||||
// 使用换行符 \n (ASCII 0x0A) 作为 SCPI 结束符
|
||||
private const string ScpiDelimiter = "\n";
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化信号发生器通信参数
|
||||
/// </summary>
|
||||
public DG1000Z(TcpConfig config) : base(config)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||
/// 支持科学计数法(如 2.48E-03),结果保留两位小数。
|
||||
/// </summary>
|
||||
/// <param name="raw">设备返回的原始字符串</param>
|
||||
/// <returns>提取到的数值(保留两位小数),解析失败时返回 0</returns>
|
||||
private static double ExtractDouble(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||
|
||||
// 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头
|
||||
var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||
? Math.Round(val, 2)
|
||||
: 0;
|
||||
}
|
||||
|
||||
#region 1. IEEE 488.2 公共命令
|
||||
|
||||
/// <summary>
|
||||
/// 清除标准事件状态寄存器和错误队列 (*CLS)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 清除状态寄存器(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询设备识别信息 (*IDN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>设备标识字符串 (制造商, 型号, 序列号, 固件版本)</returns>
|
||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重设仪器为初始状态 (*RST)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 重置设备(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询仪器支持的 SCPI 版本 (SYSTem:VERSion? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>SCPI 版本号字符串</returns>
|
||||
public virtual async Task<string> 查询系统SCPI版本(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":SYSTem:VERSion?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
@@ -82,16 +124,23 @@ namespace DeviceCommand.Devices
|
||||
#region 2. 通道输出控制 (CH1 / CH2)
|
||||
|
||||
/// <summary>
|
||||
/// 打开或关闭指定通道的输出
|
||||
/// 打开或关闭指定通道的输出 (OUTPut 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="开启">true为开启,false为关闭</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置通道输出状态(int 通道号 = 1, bool 开启 = true, CancellationToken ct = default)
|
||||
{
|
||||
string 参数 = 开启 ? "ON" : "OFF";
|
||||
await SendAsync($":OUTPut{通道号}:STATe {参数}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定通道的输出开关状态 (OUTPut:STATe? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>通道输出状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询通道输出状态(int 通道号 = 1, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":OUTPut{通道号}:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
@@ -104,51 +153,84 @@ namespace DeviceCommand.Devices
|
||||
/// <summary>
|
||||
/// 设置指定通道的波形类型 (如正弦波、方波等)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="波形">波形类型枚举,默认正弦波</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置波形类型(int 通道号 = 1, SignalWaveform 波形 = SignalWaveform.SINusoid, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":SOURce{通道号}:FUNCtion:SHAPe {波形}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定通道的输出频率 (单位: Hz)
|
||||
/// 设置指定通道的输出频率 (SOURce:FREQuency 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="频率Hz">输出频率值 (单位: Hz),默认 1000</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置频率(int 通道号 = 1, double 频率Hz = 1000, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:FREQuency {1:F2}{2}", 通道号, 频率Hz, ScpiDelimiter);
|
||||
await SendAsync(cmd, ct);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询频率(int 通道号 = 1, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的当前输出频率 (SOURce:FREQuency? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>输出频率值 (单位: Hz,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询频率(int 通道号 = 1, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":SOURce{通道号}:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($":SOURce{通道号}:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定通道的输出幅度 (单位: Vpp,峰峰值)
|
||||
/// 设置指定通道的输出幅度 (SOURce:VOLTage:AMPLitude 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="幅度Vpp">输出幅度 (单位: Vpp 峰峰值),默认 5</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置幅度(int 通道号 = 1, double 幅度Vpp = 5, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:VOLTage:AMPLitude {1:F2}{2}", 通道号, 幅度Vpp, ScpiDelimiter);
|
||||
await SendAsync(cmd, ct);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询幅度(int 通道号 = 1, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的当前输出幅度 (SOURce:VOLTage:AMPLitude? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>输出幅度 (单位: Vpp,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询幅度(int 通道号 = 1, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":SOURce{通道号}:VOLTage:AMPLitude?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($":SOURce{通道号}:VOLTage:AMPLitude?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定通道的直流偏移电压 (单位: Vdc)
|
||||
/// 设置指定通道的直流偏移电压 (SOURce:VOLTage:OFFSet 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="偏移Vdc">直流偏移电压 (单位: Vdc),默认 0</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置偏移电压(int 通道号 = 1, double 偏移Vdc = 0, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:VOLTage:OFFSet {1:F2}{2}", 通道号, 偏移Vdc, ScpiDelimiter);
|
||||
await SendAsync(cmd, ct);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询偏移电压(int 通道号 = 1, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的当前直流偏移电压 (SOURce:VOLTage:OFFSet? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>直流偏移电压 (单位: Vdc,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询偏移电压(int 通道号 = 1, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":SOURce{通道号}:VOLTage:OFFSet?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($":SOURce{通道号}:VOLTage:OFFSet?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -156,8 +238,11 @@ namespace DeviceCommand.Devices
|
||||
#region 4. 方波 / 脉冲波占空比设置 (重点用于CP信号模拟)
|
||||
|
||||
/// <summary>
|
||||
/// 设置方波信号的占空比 (0.01% ~ 99.99%)[cite: 1]
|
||||
/// 设置方波信号的占空比 (0.01% ~ 99.99%)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="占空比">方波占空比百分比,默认 50.0</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置方波占空比(int 通道号 = 1, double 占空比 = 50.0, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:FUNCtion:SQUare:DCYCle {1:F2}{2}", 通道号, 占空比, ScpiDelimiter);
|
||||
@@ -165,9 +250,12 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置脉冲波信号的占空比 (0.01% ~ 99.99%)[cite: 1]
|
||||
/// 设置脉冲波信号的占空比 (0.01% ~ 99.99%)。
|
||||
/// 注意:使用此命令前必须先设置为 PULSe 波形类型
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道 1 或 2,默认 1</param>
|
||||
/// <param name="占空比">脉冲波占空比百分比,默认 50.0</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置脉冲波占空比(int 通道号 = 1, double 占空比 = 50.0, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce{0}:FUNCtion:PULSe:DCYCle {1:F2}{2}", 通道号, 占空比, ScpiDelimiter);
|
||||
@@ -179,47 +267,68 @@ namespace DeviceCommand.Devices
|
||||
#region 5. 频率计测量功能 (高频数据轮询核心)
|
||||
|
||||
/// <summary>
|
||||
/// 打开或关闭内置频率计功能
|
||||
/// 打开或关闭内置频率计功能 (COUNter:STATe 指令)
|
||||
/// </summary>
|
||||
/// <param name="开启">true: 开启频率计, false: 关闭频率计</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置频率计开关(bool 开启 = true, CancellationToken ct = default)
|
||||
{
|
||||
string 参数 = 开启 ? "ON" : "OFF";
|
||||
await SendAsync($":COUNter:STATe {参数}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询内置频率计的开关状态 (COUNter:STATe? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>频率计开关状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询频率计开关状态(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":COUNter:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置频率计输入信号的耦合方式
|
||||
/// 设置频率计输入信号的耦合方式 (COUNter:COUPling 指令)
|
||||
/// </summary>
|
||||
/// <param name="耦合">耦合方式枚举 (AC 交流 / DC 直流),默认 AC</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置频率计耦合方式(CounterCoupling 耦合 = CounterCoupling.AC, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":COUNter:COUPling {耦合}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置频率计的触发电平 (单位: V)
|
||||
/// 设置频率计的触发电平 (COUNter:LEVel 指令)
|
||||
/// </summary>
|
||||
/// <param name="电平V">触发电平值 (单位: V),默认 0.1</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置频率计触发电平(double 电平V = 0.1, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":COUNter:LEVel {0:F2}{1}", 电平V, ScpiDelimiter);
|
||||
await SendAsync(cmd, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询频率计测量值 (COUNter:MEASure? 指令)。
|
||||
/// 响应格式为 "频率, 周期, 占空比, 正脉宽, 负脉宽",此方法提取第一项频率值
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>频率计测得的频率值 (单位: Hz,保留两位小数)</returns>
|
||||
[Monitorable("信号发生器频率计测量值")]
|
||||
public virtual async Task<string> 查询频率计测量值(CancellationToken ct = default)
|
||||
public virtual async Task<double> 查询频率计测量值(CancellationToken ct = default)
|
||||
{
|
||||
// 返回格式: 频率, 周期, 占空比, 正脉宽, 负脉宽
|
||||
return await WriteReadAsync($":COUNter:MEASure?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($":COUNter:MEASure?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 6. 系统维护与远程控制扩展命令
|
||||
|
||||
/// <summary>
|
||||
/// 切换远程控制模式。信号发生器通过 LAN/USB 通信即自动进入远程模式
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
||||
{
|
||||
// 信号发生器通常通过 LAN/USB 和 VISA 通信即自动进入远程控制,
|
||||
@@ -227,12 +336,21 @@ namespace DeviceCommand.Devices
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换本地控制模式。通常通过前面板按键退出远程
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
||||
{
|
||||
// 通常通过前面板按键退出远程。不过可提供 SCPI 关闭屏幕保护等。
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询系统错误信息 (SYSTem:ERRor? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>错误代码和描述信息</returns>
|
||||
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
@@ -240,4 +358,4 @@ namespace DeviceCommand.Devices
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,39 @@ using System.Threading.Tasks;
|
||||
namespace DeviceCommand.Devices
|
||||
{
|
||||
/// <summary>
|
||||
/// IO板卡 一对一由于板卡映射不完全一样采用IOGroup
|
||||
/// IO 继电器板卡(一对一控制),基于 Modbus TCP 协议通过线圈(Coil)读写控制继电器输出。
|
||||
/// 由于多块板卡的点位映射不完全一致,实际业务中使用 <see cref="IOBoardGroup"/> 统一调度三块板卡。
|
||||
/// </summary>
|
||||
//[ACPCommand]
|
||||
public class IOBoard : ModbusTcp
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,初始化 Modbus TCP 连接配置。
|
||||
/// </summary>
|
||||
/// <param name="config">TCP 连接配置(IP、端口等)</param>
|
||||
public IOBoard(TcpConfig config) : base(config)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 写入单个输出开关(线圈)状态。
|
||||
/// </summary>
|
||||
/// <param name="站号">Modbus 从站站号</param>
|
||||
/// <param name="开始地址">线圈地址(0 起始)</param>
|
||||
/// <param name="data">开关状态(true: 吸合/开启, false: 断开/关闭)</param>
|
||||
public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
|
||||
{
|
||||
await Modbus.WriteSingleCoilAsync(站号, 开始地址, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量写入输出开关状态。先读出全部 16 个线圈当前状态,
|
||||
/// 在内存中修改指定偏移段的值后再整体写回,避免影响其他点位。
|
||||
/// </summary>
|
||||
/// <param name="站号">Modbus 从站站号</param>
|
||||
/// <param name="开始地址">起始线圈地址(0 起始)</param>
|
||||
/// <param name="datas">要写入的开关状态数组,按顺序从开始地址写入</param>
|
||||
public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
|
||||
{
|
||||
var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16);
|
||||
@@ -40,10 +57,24 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
await Modbus.WriteMultipleCoilsAsync(站号, 0, 批量写入);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取单个输出开关(线圈)状态。
|
||||
/// </summary>
|
||||
/// <param name="站号">Modbus 从站站号</param>
|
||||
/// <param name="开始地址">线圈地址(0 起始)</param>
|
||||
public async Task 读输出开关(byte 站号, ushort 开始地址)
|
||||
{
|
||||
await Modbus.ReadCoilsAsync(站号, 开始地址, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量读取输出开关(线圈)状态。
|
||||
/// </summary>
|
||||
/// <param name="站号">Modbus 从站站号</param>
|
||||
/// <param name="开始地址">起始线圈地址(0 起始)</param>
|
||||
/// <param name="数量">要读取的线圈数量</param>
|
||||
/// <returns>线圈状态数组,true 表示吸合/开启</returns>
|
||||
public async Task<bool[]> 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量)
|
||||
{
|
||||
return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量);
|
||||
|
||||
@@ -48,6 +48,11 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IO 板卡组(三工位统一调度),封装 3 块 IO 继电器板卡的访问。
|
||||
/// 根据 ACP 硬件接线图的点位映射字典,按台架序号路由到对应板卡的线圈地址,
|
||||
/// 并内置充电/放电互锁保护,防止同时吸合造成短路。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class IOBoardGroup
|
||||
{
|
||||
@@ -121,6 +126,13 @@ namespace DeviceCommand.Devices
|
||||
}}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,注入三个工位的 IO 板卡实例。
|
||||
/// </summary>
|
||||
/// <param name="board1">1# 工位 IO 板卡实例</param>
|
||||
/// <param name="board2">2# 工位 IO 板卡实例</param>
|
||||
/// <param name="board3">3# 工位 IO 板卡实例</param>
|
||||
/// <exception cref="ArgumentNullException">任一块板卡实例为空时抛出</exception>
|
||||
// 修改构造函数以支持 3 个板块
|
||||
public IOBoardGroup(IOBoard board1, IOBoard board2, IOBoard board3)
|
||||
{
|
||||
@@ -169,8 +181,24 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部路由辅助方法:根据板卡索引安全获取当前连接的实例对象
|
||||
/// 向指定板卡批量写入输出开关状态,直接透传到对应板卡的批量写线圈操作。
|
||||
/// </summary>
|
||||
/// <param name="板卡序号">板卡序号 (1 - 3)</param>
|
||||
/// <param name="站号">Modbus 从站站号</param>
|
||||
/// <param name="开始地址">线圈起始地址 (0-15)</param>
|
||||
/// <param name="数据">开关状态数组 (true: 开启吸合, false: 关闭断开),例如 [true, true, false]</param>
|
||||
/// <param name="取消令牌">异步取消令牌</param>
|
||||
public async Task 批量写输出开关(int 板卡序号, byte 站号, ushort 开始地址, bool[] 数据, CancellationToken 取消令牌 = default)
|
||||
{
|
||||
await GetBoardInstance(板卡序号).批量写输出开关(站号, 开始地址, 数据);
|
||||
}
|
||||
/// <summary>
|
||||
/// 内部路由辅助方法:根据板卡序号安全获取对应的 IO 板卡实例。
|
||||
/// </summary>
|
||||
/// <param name="moduleIndex">板卡序号 (1 - 3)</param>
|
||||
/// <returns>对应的 IO 板卡实例</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">板卡序号不在 1-3 范围内时抛出</exception>
|
||||
/// <exception cref="InvalidOperationException">板卡实例未初始化时抛出</exception>
|
||||
private IOBoard GetBoardInstance(int moduleIndex)
|
||||
{
|
||||
IOBoard board = moduleIndex switch
|
||||
|
||||
@@ -133,6 +133,10 @@ namespace DeviceCommand.Devices
|
||||
throw new Exception("IT6720 切换控制模式时通讯校验失败。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换为本地(面板)控制模式,与远程控制互斥
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
||||
{
|
||||
await 切换远程控制模式(false, ct);
|
||||
@@ -150,6 +154,11 @@ namespace DeviceCommand.Devices
|
||||
throw new Exception("IT6720 设置输出时通讯校验失败。");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询电源输出开关状态(由实时测量值的状态字节判断)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>输出开关状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询输出开关状态(CancellationToken ct = default)
|
||||
{
|
||||
var (_, _, status) = await 查询实时测量值(ct);
|
||||
@@ -230,18 +239,28 @@ namespace DeviceCommand.Devices
|
||||
return (actualCurrent, actualVoltage, statusByte);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询电源的实时实际输出电压 (MEASure 相关寄存器)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>实时电压值 (单位: V,保留两位小数)</returns>
|
||||
[Monitorable("IT6720 实际电压")]
|
||||
public virtual async Task<string> 查询实际电压(CancellationToken ct = default)
|
||||
public virtual async Task<double> 查询实际电压(CancellationToken ct = default)
|
||||
{
|
||||
var (_, voltage, _) = await 查询实时测量值(ct);
|
||||
return voltage.ToString("F2", CultureInfo.InvariantCulture);
|
||||
return Math.Round(voltage, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询电源的实时实际输出电流 (MEASure 相关寄存器)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>实时电流值 (单位: A,保留两位小数)</returns>
|
||||
[Monitorable("IT6720 实际电流")]
|
||||
public virtual async Task<string> 查询实际电流(CancellationToken ct = default)
|
||||
public virtual async Task<double> 查询实际电流(CancellationToken ct = default)
|
||||
{
|
||||
var (current, _, _) = await 查询实时测量值(ct);
|
||||
return current.ToString("F3", CultureInfo.InvariantCulture);
|
||||
return Math.Round(current, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,20 +1,401 @@
|
||||
using DeviceCommand.Base;
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Devices
|
||||
{
|
||||
/// <summary>
|
||||
/// 水冷机 一拖三
|
||||
/// MCc30W 水冷机(一拖三控制),基于 Modbus TCP 协议(MCx ModbusTCP)通信。
|
||||
/// 支持 3 回路独立控制:出液温度/流量/压力的目标设定与实时读取、回路启停、
|
||||
/// 预控温、控流/控压模式切换、自动回液、报警复位/消音及状态回读。
|
||||
/// 多寄存器数值采用 IEEE754 浮点大端序编码(2 个 16 位寄存器)。
|
||||
/// </summary>
|
||||
public class MCc30W:ModbusTcp
|
||||
[ACPCommand]
|
||||
public class MCc30W : ModbusTcp
|
||||
{
|
||||
// 默认配置 (可从 config 或 TcpConfig 注入)
|
||||
private readonly byte _slaveId = 0x01; // 单元标识符
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,初始化 Modbus TCP 连接配置。
|
||||
/// </summary>
|
||||
/// <param name="config">TCP 连接配置(IP、端口等)</param>
|
||||
public MCc30W(TcpConfig config) : base(config)
|
||||
{
|
||||
}
|
||||
|
||||
#region 1. 底层连接与字节序转换辅助
|
||||
|
||||
/// <summary>
|
||||
/// 将单精度浮点数转换为 Modbus 大端序 2个16位寄存器
|
||||
/// </summary>
|
||||
private ushort[] FloatToRegisters(float value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
return new ushort[]
|
||||
{
|
||||
BitConverter.ToUInt16(bytes, 0),
|
||||
BitConverter.ToUInt16(bytes, 2)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Modbus 大端序 2个16位寄存器转换回单精度浮点数
|
||||
/// </summary>
|
||||
private float RegistersToFloat(ushort[] registers)
|
||||
{
|
||||
if (registers.Length < 2) return 0f;
|
||||
byte[] bytes = new byte[4];
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(registers[0]), 0, bytes, 0, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(registers[1]), 0, bytes, 2, 2);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
return BitConverter.ToSingle(bytes, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 32位无符号整数转换为 Modbus 大端序 2个16位寄存器
|
||||
/// </summary>
|
||||
private ushort[] UInt32ToRegisters(uint value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
return new ushort[]
|
||||
{
|
||||
BitConverter.ToUInt16(bytes, 0),
|
||||
BitConverter.ToUInt16(bytes, 2)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Modbus 大端序 2个16位寄存器转换回 32位无符号整数
|
||||
/// </summary>
|
||||
private uint RegistersToUInt32(ushort[] registers)
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(registers[0]), 0, bytes, 0, 2);
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(registers[1]), 0, bytes, 2, 2);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
|
||||
return BitConverter.ToUInt32(bytes, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 2. 模拟量读写 API (支持 3 通道)
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定回路的出液目标温度,写入对应回路的保持寄存器。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="temperature">出液目标温度,单位 ℃</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置出液目标温度Async(int loop, float temperature, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0000 + (loop - 1) * 2);
|
||||
ushort[] data = FloatToRegisters(temperature);
|
||||
await WriteMultipleRegistersAsync(_slaveId, address, data, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定回路的出液目标流量,写入对应回路的保持寄存器。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="flow">出液目标流量,单位 L/min</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置出液目标流量Async(int loop, float flow, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0008 + (loop - 1) * 2);
|
||||
ushort[] data = FloatToRegisters(flow);
|
||||
await WriteMultipleRegistersAsync(_slaveId, address, data, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定回路的出液目标压力,写入对应回路的保持寄存器。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="pressure">出液目标压力,单位 kPa</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 设置出液目标压力Async(int loop, float pressure, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x000E + (loop - 1) * 2);
|
||||
ushort[] data = FloatToRegisters(pressure);
|
||||
await WriteMultipleRegistersAsync(_slaveId, address, data, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的当前出液温度。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>当前出液温度,单位 ℃</returns>
|
||||
[Monitorable("水冷机出液当前温度")]
|
||||
public virtual async Task<float> 读取出液当前温度Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0018 + (loop - 1) * 2);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的当前出液流量。
|
||||
/// 注意:协议中三通道流量地址间隔为 0x08。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>当前出液流量,单位 L/min</returns>
|
||||
[Monitorable("水冷机出液当前流量")]
|
||||
public virtual async Task<float> 读取出液当前流量Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x001E + (loop - 1) * 8);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的当前出液压力。
|
||||
/// 注意:协议中三通道压力地址间隔为 0x08。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>当前出液压力,单位 kPa</returns>
|
||||
[Monitorable("水冷机出液当前压力")]
|
||||
public virtual async Task<float> 读取出液当前压力Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0020 + (loop - 1) * 8);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的当前回液压力(寄存器地址 0x0022 + (loop-1)*8)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>当前回液压力,单位 kPa</returns>
|
||||
[Monitorable("水冷机回液当前压力")]
|
||||
public virtual async Task<float> 读取回液当前压力Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0022 + (loop - 1) * 8);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的当前回液温度(寄存器地址 0x0024 + (loop-1)*8)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>当前回液温度,单位 ℃</returns>
|
||||
[Monitorable("水冷机回液当前温度")]
|
||||
public virtual async Task<float> 读取回液当前温度Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0024 + (loop - 1) * 8);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的外部温度反馈(寄存器地址 0x0036 + (loop-1)*2)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>外部温度反馈值,单位 ℃</returns>
|
||||
[Monitorable("水冷机外部温度反馈")]
|
||||
public virtual async Task<float> 读取外部温度反馈Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0036 + (loop - 1) * 2);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定回路的温变速率设定值(寄存器地址 0x0006 + (loop-1)*2)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3)</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>温变速率设定值,单位 ℃/min</returns>
|
||||
[Monitorable("水冷机温变速率")]
|
||||
public virtual async Task<float> 读取温变速率Async(int loop, CancellationToken ct = default)
|
||||
{
|
||||
ushort address = (ushort)(0x0006 + (loop - 1) * 2);
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取回抽2流量目标值(固定寄存器地址 0x0016,不区分回路)。
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>回抽2流量目标值,单位 L/min</returns>
|
||||
[Monitorable("水冷机回抽2流量目标值")]
|
||||
public virtual async Task<float> 读取回抽2流量目标值Async(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x0016, 2, ct);
|
||||
return RegistersToFloat(regs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 3. 系统状态与位控制 API (支持 3 通道)
|
||||
|
||||
/// <summary>
|
||||
/// 核心位操作方法:读取 -> 置位/复位 -> 写入系统状态控制寄存器 (0x0014)
|
||||
/// </summary>
|
||||
private async Task SetControlBit(int bitIndex, bool isSet, CancellationToken ct = default)
|
||||
{
|
||||
// 1. 读取当前状态
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x0014, 1, ct);
|
||||
ushort currentState = regs[0];
|
||||
|
||||
// 2. 修改位
|
||||
if (isSet)
|
||||
currentState |= (ushort)(1 << bitIndex);
|
||||
else
|
||||
currentState &= (ushort)~(1 << bitIndex);
|
||||
|
||||
// 3. 写回
|
||||
await WriteSingleRegisterAsync(_slaveId, 0x0014, currentState, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动/停止指定回路(操作系统状态控制寄存器 0x0014 的 Bit 01/02/03)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3),分别对应 Bit 1/2/3</param>
|
||||
/// <param name="enable">true: 启动回路, false: 停止回路</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">回路号不在 1-3 范围内时抛出</exception>
|
||||
public virtual async Task 设置回路运行Async(int loop, bool enable, CancellationToken ct = default)
|
||||
{
|
||||
if (loop < 1 || loop > 3) throw new ArgumentOutOfRangeException(nameof(loop), "回路号必须为1, 2, 3");
|
||||
await SetControlBit(loop, enable, ct); // 回路1->Bit1, 回路2->Bit2, 回路3->Bit3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动/停止指定回路的预控温(操作系统状态控制寄存器 0x0014 的 Bit 10/11/12)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3),分别对应 Bit 10/11/12</param>
|
||||
/// <param name="enable">true: 开启预控温, false: 关闭预控温</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">回路号不在 1-3 范围内时抛出</exception>
|
||||
public virtual async Task 设置回路预控温Async(int loop, bool enable, CancellationToken ct = default)
|
||||
{
|
||||
if (loop < 1 || loop > 3) throw new ArgumentOutOfRangeException(nameof(loop), "回路号必须为1, 2, 3");
|
||||
await SetControlBit(9 + loop, enable, ct); // 回路1->Bit10, 回路2->Bit11, 回路3->Bit12
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定回路的流量/压力控制模式(操作系统状态控制寄存器 0x0014 的 Bit 04/05/06)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3),分别对应 Bit 4/5/6</param>
|
||||
/// <param name="isPressureControl">true: 控压模式, false: 控流模式</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">回路号不在 1-3 范围内时抛出</exception>
|
||||
public virtual async Task 设置回路压力控制Async(int loop, bool isPressureControl, CancellationToken ct = default)
|
||||
{
|
||||
if (loop < 1 || loop > 3) throw new ArgumentOutOfRangeException(nameof(loop), "回路号必须为1, 2, 3");
|
||||
await SetControlBit(3 + loop, isPressureControl, ct); // 回路1->Bit4, 回路2->Bit5, 回路3->Bit6
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 报警复位(操作系统状态控制寄存器 0x0014 的 Bit 08),用于清除水冷机报警状态。
|
||||
/// </summary>
|
||||
/// <param name="enable">true: 置位报警复位位, false: 清除该位</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 报警复位Async(bool enable, CancellationToken ct = default)
|
||||
{
|
||||
await SetControlBit(8, enable, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 报警消音(操作系统状态控制寄存器 0x0014 的 Bit 09),用于关闭报警蜂鸣。
|
||||
/// </summary>
|
||||
/// <param name="enable">true: 置位消音位, false: 清除该位</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public virtual async Task 报警消音Async(bool enable, CancellationToken ct = default)
|
||||
{
|
||||
await SetControlBit(9, enable, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定回路的自动回液开关(操作寄存器 0x0015 的 Bit 13/14/15)。
|
||||
/// </summary>
|
||||
/// <param name="loop">回路编号 (1-3),分别对应 Bit 13/14/15</param>
|
||||
/// <param name="enable">true: 开启自动回液, false: 关闭自动回液</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">回路号不在 1-3 范围内时抛出</exception>
|
||||
public virtual async Task 设置回路自动回液Async(int loop, bool enable, CancellationToken ct = default)
|
||||
{
|
||||
if (loop < 1 || loop > 3) throw new ArgumentOutOfRangeException(nameof(loop), "回路号必须为1, 2, 3");
|
||||
// 协议 Sheet2 中 0x0015 寄存器位定义:
|
||||
// Bit 13 (Loop1), Bit 14 (Loop2), Bit 15 (Loop3)
|
||||
int bitIndex = 12 + loop;
|
||||
// 0x0015 控制寄存器逻辑
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x0015, 1, ct);
|
||||
ushort currentState = regs[0];
|
||||
|
||||
if (enable)
|
||||
currentState |= (ushort)(1 << bitIndex);
|
||||
else
|
||||
currentState &= (ushort)~(1 << bitIndex);
|
||||
|
||||
await WriteSingleRegisterAsync(_slaveId, 0x0015, currentState, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 4. 状态回读 (报警、心跳、运行状态)
|
||||
|
||||
/// <summary>
|
||||
/// 读取心跳帧(寄存器 0x0044),心跳值持续变化,用于判断通讯是否正常。
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>32 位心跳计数值</returns>
|
||||
[Monitorable("水冷机心跳帧")]
|
||||
public virtual async Task<uint> 读心跳帧Async(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x0044, 2, ct);
|
||||
return RegistersToUInt32(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取运行状态反馈(寄存器 0x0046),含各回路完成状态位(协议 Sheet5)。
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>32 位运行状态位图</returns>
|
||||
[Monitorable("水冷机运行状态反馈")]
|
||||
public virtual async Task<uint> 读运行状态反馈Async(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x0046, 2, ct);
|
||||
return RegistersToUInt32(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取报警信息1(寄存器 0x003C),每一位对应一种报警类型。
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>32 位报警信息位图,0 表示无报警</returns>
|
||||
[Monitorable("水冷机报警信息1")]
|
||||
public virtual async Task<uint> 读报警信息1Async(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x003C, 2, ct);
|
||||
return RegistersToUInt32(regs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取报警信息2(寄存器 0x003E),每一位对应一种报警类型。
|
||||
/// </summary>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>32 位报警信息位图,0 表示无报警</returns>
|
||||
[Monitorable("水冷机报警信息2")]
|
||||
public virtual async Task<uint> 读报警信息2Async(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] regs = await ReadHoldingRegistersAsync(_slaveId, 0x003E, 2, ct);
|
||||
return RegistersToUInt32(regs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
+262
-34
@@ -2,15 +2,18 @@
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Device
|
||||
{
|
||||
/// <summary>
|
||||
/// 功率分析仪(型号:HIOKI PW8001) 一拖三
|
||||
/// 功率分析仪(型号:HIOKI PW8001) 一拖三,支持多通道电压/电流/功率及谐波测量
|
||||
/// </summary>
|
||||
|
||||
[ACPCommand]
|
||||
public class PW8001 : Tcp
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,23 +28,58 @@ namespace DeviceCommand.Device
|
||||
/// </summary>
|
||||
public string? SessionKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||
/// 支持科学计数法(如 2.48E-03),结果保留两位小数。
|
||||
/// </summary>
|
||||
/// <param name="raw">设备返回的原始字符串</param>
|
||||
/// <returns>提取到的数值(保留两位小数),解析失败时返回 0</returns>
|
||||
private static double ExtractDouble(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||
|
||||
// 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头
|
||||
var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||
? Math.Round(val, 2)
|
||||
: 0;
|
||||
}
|
||||
|
||||
#region 1. IEEE 488.2 基础标准命令
|
||||
|
||||
/// <summary>
|
||||
/// 查询仪器识别信息 (*IDN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>设备标识字符串 (制造商, 型号, 序列号, 固件版本)</returns>
|
||||
public virtual async Task<string> 查询机器信息(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync("*IDN?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询仪器已安装的硬件选件 (*OPT? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>硬件选件列表字符串</returns>
|
||||
public virtual async Task<string> 查询硬件选项(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync("*OPT?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复位仪器到默认状态 (*RST 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 复位仪器(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync("*RST\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除状态寄存器和错误队列 (*CLS 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 清除状态(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync("*CLS\r\n", ct);
|
||||
@@ -51,66 +89,129 @@ namespace DeviceCommand.Device
|
||||
|
||||
#region 2. 仪器系统与测试模式设置设置 (System & Mode)
|
||||
|
||||
/// <summary>
|
||||
/// 设置测试模式为 WIDE 宽带测量模式 (:MODE WIDE 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置测试模式_WIDE(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync(":MODE WIDE\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置测试模式为 IEC 标准测量模式 (:MODE IEC 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置测试模式_IEC(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync(":MODE IEC\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询当前测试模式 (:MODE? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>测试模式字符串 (WIDE / IEC)</returns>
|
||||
public virtual async Task<string> 查询测试模式(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":MODE?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置同步源 (:SYNC:SOURce 指令)
|
||||
/// </summary>
|
||||
/// <param name="源">同步源标识</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置同步源(string 源, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":SYNC:SOURce {源}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询当前同步源 (:SYNC:SOURce? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>同步源标识字符串</returns>
|
||||
public virtual async Task<string> 查询同步源(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":SYNC:SOURce?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置零点调整状态 (:ZERO 指令)
|
||||
/// </summary>
|
||||
/// <param name="状态">零点调整状态 (ON / OFF)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置零点调整(string 状态, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":ZERO {状态}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询零点调整状态 (:ZERO? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>零点调整状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询零点调整(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":ZERO?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置零点抑制状态 (:ZSP 指令)
|
||||
/// </summary>
|
||||
/// <param name="状态">零点抑制状态 (ON / OFF)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置零点抑制(string 状态, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":ZSP {状态}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询零点抑制状态 (:ZSP? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>零点抑制状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询零点抑制(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":ZSP?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置响应消息头开关 (:HEADer 指令)
|
||||
/// </summary>
|
||||
/// <param name="状态">消息头开关状态 (ON / OFF)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置响应消息头(string 状态, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":HEADer {状态}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询响应消息头开关状态 (:HEADer? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>消息头开关状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询响应消息头(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":HEADer?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置按键锁定状态 (:KLOCk 指令)
|
||||
/// </summary>
|
||||
/// <param name="状态">按键锁定状态 (ON / OFF)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置按键锁定(string 状态, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":KLOCk {状态}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询按键锁定状态 (:KLOCk? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>按键锁定状态字符串 (ON / OFF)</returns>
|
||||
public virtual async Task<string> 查询按键锁定(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":KLOCk?\r\n", "\n", ct);
|
||||
@@ -120,21 +221,43 @@ namespace DeviceCommand.Device
|
||||
|
||||
#region 3. 积分控制 (Integration)
|
||||
|
||||
/// <summary>
|
||||
/// 设置积分控制模式 (:INTEG:CONTROL 指令)
|
||||
/// </summary>
|
||||
/// <param name="模式">积分控制模式</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置积分控制(string 模式, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":INTEG:CONTROL {模式}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分控制模式 (:INTEG:CONTROL? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>积分控制模式字符串</returns>
|
||||
public virtual async Task<string> 查询积分控制(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":INTEG:CONTROL?\r\n", "\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定通道的积分模式 (:INTEG:MODE 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="模式">积分模式</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置积分模式(string 通道号, string 模式, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($":INTEG:MODE{通道号} {模式}\r\n", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定通道的积分模式 (:INTEG:MODE? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>积分模式字符串</returns>
|
||||
public virtual async Task<string> 查询积分模式(string 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":INTEG:MODE{通道号}?\r\n", "\n", ct);
|
||||
@@ -146,85 +269,188 @@ namespace DeviceCommand.Device
|
||||
|
||||
// ---------------- 基础测量(不含变比) ----------------
|
||||
|
||||
public virtual async Task<string> 查询电压_不含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电压测量值(不含变比) (:MEAS:U? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电压测量值 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电压_不含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:U{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:U{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电流_不含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电流测量值(不含变比) (:MEAS:I? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电流测量值 (单位: A,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电流_不含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:I{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:I{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询功率_不含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的功率测量值(不含变比) (:MEAS:P? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>功率测量值 (单位: W,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询功率_不含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:P{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:P{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
// ---------------- 基础测量(含变比 CT) ----------------
|
||||
|
||||
public virtual async Task<string> 查询电压_含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电压测量值(含变比) (:MEAS:U:CT? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电压测量值 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电压_含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:U{通道号}:CT?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:U{通道号}:CT?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电流_含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电流测量值(含变比) (:MEAS:I:CT? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电流测量值 (单位: A,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电流_含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:I{通道号}:CT?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:I{通道号}:CT?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询功率_含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的功率测量值(含变比) (:MEAS:P:CT? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>功率测量值 (单位: W,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询功率_含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:P{通道号}:CT?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:P{通道号}:CT?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
// ---------------- 其他高阶参数测量 ----------------
|
||||
|
||||
public virtual async Task<string> 查询有功功率积分(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的有功功率积分值 (:MEAS:WP? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>有功功率积分值 (单位: Wh,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询有功功率积分(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:WP{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:WP{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电压THD(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电压总谐波失真 (THD) (:MEAS:UTHD? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电压 THD 值 (单位: %,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电压THD(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:UTHD{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:UTHD{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电流THD(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电流总谐波失真 (THD) (:MEAS:ITHD? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电流 THD 值 (单位: %,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电流THD(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:ITHD{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:ITHD{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询线电压U12(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询线电压 U12 (:MEAS:U12? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>线电压 U12 值 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询线电压U12(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":MEAS:U12?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync(":MEAS:U12?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询总功率P123(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询三相总功率 P123 (:MEAS:P123? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>三相总功率值 (单位: W,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询总功率P123(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync(":MEAS:P123?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync(":MEAS:P123?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
// ---------------- 整流平均值 ----------------
|
||||
|
||||
public virtual async Task<string> 查询电压整流平均值(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电压整流平均值(不含变比) (:MEAS:UMN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电压整流平均值 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电压整流平均值(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:UMN{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:UMN{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电流整流平均值(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电流整流平均值(不含变比) (:MEAS:IMN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电流整流平均值 (单位: A,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电流整流平均值(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:IMN{通道号}?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:IMN{通道号}?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电压整流平均值_含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电压整流平均值(含变比) (:MEAS:UMN:CT? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电压整流平均值 (单位: V,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电压整流平均值_含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:UMN{通道号}:CT?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:UMN{通道号}:CT?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询电流整流平均值_含变比(int 通道号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询指定通道的电流整流平均值(含变比) (:MEAS:IMN:CT? 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>电流整流平均值 (单位: A,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询电流整流平均值_含变比(int 通道号, CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($":MEAS:IMN{通道号}:CT?\r\n", "\n", ct);
|
||||
string resp = await WriteReadAsync($":MEAS:IMN{通道号}:CT?\r\n", "\n", ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -232,8 +458,10 @@ namespace DeviceCommand.Device
|
||||
#region 5. 自定义透传指令
|
||||
|
||||
/// <summary>
|
||||
/// 发送自定义命令
|
||||
/// 发送自定义命令(透传任意 SCPI 指令)
|
||||
/// </summary>
|
||||
/// <param name="命令">自定义命令字符串</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 发送自定义命令(string 命令, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"{命令}\r\n", ct);
|
||||
@@ -241,4 +469,4 @@ namespace DeviceCommand.Device
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,30 +8,45 @@ using System.Threading.Tasks;
|
||||
namespace DeviceCommand.Devices
|
||||
{
|
||||
/// <summary>
|
||||
/// 环境箱(型号:RLT1000) 一拖三
|
||||
/// 环境箱(型号:RLT1000) 一拖三,支持温度/湿度/水温/水流量的监测与定值控制 (Modbus TCP 协议)
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class RLT1000 : ModbusTcp
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数:传入 <see cref="TcpConfig"/> 初始化环境箱 Modbus TCP 通信参数
|
||||
/// </summary>
|
||||
public RLT1000(TcpConfig config) : base(config)
|
||||
{
|
||||
}
|
||||
|
||||
// Modbus 从站号
|
||||
private const byte SlaveId = 1;
|
||||
|
||||
#region 读取
|
||||
|
||||
/// <summary>
|
||||
/// 读取环境箱运行状态监控字 (保持寄存器 5)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>状态监控字 (ushort 原始值)</returns>
|
||||
public async Task<ushort> 读取状态监控(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 5, 1, ct);
|
||||
return re[0];
|
||||
}
|
||||
|
||||
public async Task<float> 读取当前温度(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 读取环境箱当前温度值 (保持寄存器 10-11)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>当前温度值 (单位: ℃,保留两位小数)</returns>
|
||||
public async Task<double> 读取当前温度(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 10, 2, ct);
|
||||
return ConvertToFloat(re);
|
||||
return Math.Round(ConvertToFloat(re), 2);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -39,18 +54,28 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<float> 读取当前湿度(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 读取环境箱当前湿度值 (保持寄存器 16-17)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>当前湿度值 (单位: %RH,保留两位小数)</returns>
|
||||
public async Task<double> 读取当前湿度(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 16, 2, ct);
|
||||
return ConvertToFloat(re);
|
||||
return Math.Round(ConvertToFloat(re), 2);
|
||||
}
|
||||
|
||||
public async Task<float> 读取当前水温(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 读取环境箱当前水温值 (保持寄存器 22-23)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>当前水温值 (单位: ℃,保留两位小数)</returns>
|
||||
public async Task<double> 读取当前水温(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 22, 2, ct);
|
||||
return ConvertToFloat(re);
|
||||
return Math.Round(ConvertToFloat(re), 2);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -58,119 +83,212 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<float> 读取当前1号水流量值(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 读取当前 1 号水流量值 (保持寄存器 28-29)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>1 号水流量值 (保留两位小数)</returns>
|
||||
public async Task<double> 读取当前1号水流量值(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 28, 2, ct);
|
||||
return ConvertToFloat(re);
|
||||
return Math.Round(ConvertToFloat(re), 2);
|
||||
}
|
||||
|
||||
public async Task<float> 读取当前2号水流量值(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 读取当前 2 号水流量值 (保持寄存器 34-35)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>2 号水流量值 (保留两位小数)</returns>
|
||||
public async Task<double> 读取当前2号水流量值(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 34, 2, ct);
|
||||
return ConvertToFloat(re);
|
||||
return Math.Round(ConvertToFloat(re), 2);
|
||||
}
|
||||
|
||||
public async Task<float> 读取当前3号水流量值(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 读取当前 3 号水流量值 (保持寄存器 40-41)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>3 号水流量值 (保留两位小数)</returns>
|
||||
public async Task<double> 读取当前3号水流量值(CancellationToken ct = default)
|
||||
{
|
||||
ushort[] re = await ReadHoldingRegistersAsync(SlaveId, 40, 2, ct);
|
||||
return ConvertToFloat(re);
|
||||
return Math.Round(ConvertToFloat(re), 2);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 读写(控制命令)
|
||||
|
||||
/// <summary>
|
||||
/// 切换环境箱为本地控制模式 (寄存器 147 写 0)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 切换为本地模式(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 147, 0, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换环境箱为远程控制模式 (寄存器 147 写 1)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 切换为远程模式(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 147, 1, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程模式下关闭环境箱 (寄存器 148 写 0)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 远程模式关机(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 148, 0, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程模式下开启环境箱 (寄存器 148 写 1)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 远程模式开机(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 148, 1, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程模式复位结束 (寄存器 149 写 0)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 远程模式复位结束(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 149, 0, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 远程模式复位 (寄存器 149 写 1)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 远程模式复位(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 149, 1, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换为程序运行模式 (寄存器 153 写 0)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 切换为程序模式(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 153, 0, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换为定值运行模式 (寄存器 153 写 1)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 切换为定值模式(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 153, 1, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定值模式下设置目标温度 (寄存器 154-155)
|
||||
/// </summary>
|
||||
/// <param name="温度">目标温度值 (单位: ℃)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 定值温度设定(float 温度, CancellationToken ct = default)
|
||||
{
|
||||
var tmp = ConvertFromFloat(温度);
|
||||
await WriteMultipleRegistersAsync(SlaveId, 154, tmp, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定值模式下设置目标湿度 (寄存器 158-159)
|
||||
/// </summary>
|
||||
/// <param name="湿度">目标湿度值 (单位: %RH)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 定值湿度设定(float 湿度, CancellationToken ct = default)
|
||||
{
|
||||
var tmp = ConvertFromFloat(湿度);
|
||||
await WriteMultipleRegistersAsync(SlaveId, 158, tmp, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定值模式下设置目标水温 (寄存器 162-163)
|
||||
/// </summary>
|
||||
/// <param name="水温">目标水温值 (单位: ℃)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 定值水温设定(float 水温, CancellationToken ct = default)
|
||||
{
|
||||
var tmp = ConvertFromFloat(水温);
|
||||
await WriteMultipleRegistersAsync(SlaveId, 162, tmp, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定值模式下设置 1 号水流量目标值 (寄存器 164-165)
|
||||
/// </summary>
|
||||
/// <param name="水流量">1 号水流量目标值</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 定值1号水流量设定(float 水流量, CancellationToken ct = default)
|
||||
{
|
||||
var tmp = ConvertFromFloat(水流量);
|
||||
await WriteMultipleRegistersAsync(SlaveId, 164, tmp, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定值模式下设置 2 号水流量目标值 (寄存器 166-167)
|
||||
/// </summary>
|
||||
/// <param name="水流量">2 号水流量目标值</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 定值2号水流量设定(float 水流量, CancellationToken ct = default)
|
||||
{
|
||||
var tmp = ConvertFromFloat(水流量);
|
||||
await WriteMultipleRegistersAsync(SlaveId, 166, tmp, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定值模式下设置 3 号水流量目标值 (寄存器 168-169)
|
||||
/// </summary>
|
||||
/// <param name="水流量">3 号水流量目标值</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 定值3号水流量设定(float 水流量, CancellationToken ct = default)
|
||||
{
|
||||
var tmp = ConvertFromFloat(水流量);
|
||||
await WriteMultipleRegistersAsync(SlaveId, 168, tmp, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置水温模式不使用 (寄存器 1181 写 0)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 水温模式不使用(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 1181, 0, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置水温模式使用 (寄存器 1181 写 1)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 水温模式使用(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 1181, 1, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置环境箱温度模式不使用 (寄存器 1182 写 0)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 环境箱温度模式不使用(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 1182, 0, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置环境箱温度模式使用 (寄存器 1182 写 1)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 环境箱温度模式使用(CancellationToken ct = default)
|
||||
{
|
||||
await WriteSingleRegisterAsync(SlaveId, 1182, 1, ct);
|
||||
@@ -180,6 +298,11 @@ namespace DeviceCommand.Devices
|
||||
|
||||
#region 数据转换助手方法
|
||||
|
||||
/// <summary>
|
||||
/// 将两个 16 位寄存器(大端序)转换为 IEEE754 单精度浮点数
|
||||
/// </summary>
|
||||
/// <param name="values">两个 ushort 寄存器值</param>
|
||||
/// <returns>转换后的浮点数值</returns>
|
||||
private float ConvertToFloat(ushort[] values)
|
||||
{
|
||||
if (values == null || values.Length < 2) return 0f;
|
||||
@@ -192,6 +315,11 @@ namespace DeviceCommand.Devices
|
||||
return BitConverter.ToSingle(bytes, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将单精度浮点数转换为两个 16 位寄存器(大端序)
|
||||
/// </summary>
|
||||
/// <param name="value">待转换的浮点数值</param>
|
||||
/// <returns>两个 ushort 寄存器值</returns>
|
||||
private ushort[] ConvertFromFloat(float value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
@@ -203,4 +331,4 @@ namespace DeviceCommand.Devices
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+235
-37
@@ -1,65 +1,119 @@
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
// 根据您的项目实际路径保留枚举的引用
|
||||
// using 通讯驱动Base.枚举;
|
||||
|
||||
namespace DeviceCommand.Devices
|
||||
{
|
||||
/// <summary>
|
||||
/// S7200 负载工作模式枚举 (恒流/恒压/恒功率/恒阻等)
|
||||
/// </summary>
|
||||
public enum S7200负载模式_枚举
|
||||
{
|
||||
/// <summary>恒流模式</summary>
|
||||
CC,
|
||||
/// <summary>恒压模式</summary>
|
||||
CV,
|
||||
/// <summary>恒功率模式</summary>
|
||||
CP,
|
||||
/// <summary>恒阻模式</summary>
|
||||
CR,
|
||||
/// <summary>恒压+恒流组合模式</summary>
|
||||
CVCC,
|
||||
/// <summary>恒阻+恒流组合模式</summary>
|
||||
CRCC,
|
||||
/// <summary>恒压+恒阻组合模式</summary>
|
||||
CVCR,
|
||||
/// <summary>自动模式</summary>
|
||||
AUTO
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S7200 直流源工作模式枚举 (电压源/电流源/电池模拟/光伏模拟等)
|
||||
/// </summary>
|
||||
public enum S7200直流源模式_枚举
|
||||
{
|
||||
/// <summary>电压源模式</summary>
|
||||
VOLTage,
|
||||
/// <summary>电流源模式</summary>
|
||||
CURRent,
|
||||
/// <summary>电池测试模式</summary>
|
||||
BATTest,
|
||||
/// <summary>电池模拟模式</summary>
|
||||
BATSim,
|
||||
/// <summary>光伏模拟模式</summary>
|
||||
SOLar,
|
||||
/// <summary>EN50530 标准光伏曲线模式</summary>
|
||||
EN50530,
|
||||
/// <summary>列表输出模式</summary>
|
||||
LIST,
|
||||
/// <summary>用户自定义波形模式</summary>
|
||||
UDW,
|
||||
/// <summary>方波输出模式</summary>
|
||||
SQUare,
|
||||
/// <summary>三角波输出模式</summary>
|
||||
TRIangle,
|
||||
/// <summary>正弦波输出模式</summary>
|
||||
SINusoid,
|
||||
/// <summary>DIN40839 12V 启动曲线</summary>
|
||||
DIN40839_12V,
|
||||
/// <summary>DIN40839 24V 启动曲线</summary>
|
||||
DIN40839_24V,
|
||||
/// <summary>ISO16750 短时跌落 12V</summary>
|
||||
IS016750_SHORTDROP_12V,
|
||||
/// <summary>ISO16750 短时跌落 24V</summary>
|
||||
IS016750_SHORTDROP_24V,
|
||||
/// <summary>ISO16750 复位测试</summary>
|
||||
IS016750_RESETTEST,
|
||||
/// <summary>ISO16750 启动曲线 12V 第 1 组</summary>
|
||||
IS016750_STARTINGPROFILE_12V_1,
|
||||
/// <summary>ISO16750 启动曲线 12V 第 2 组</summary>
|
||||
IS016750_STARTINGPROFILE_12V_2,
|
||||
/// <summary>ISO16750 启动曲线 12V 第 3 组</summary>
|
||||
IS016750_STARTINGPROFILE_12V_3,
|
||||
/// <summary>ISO16750 启动曲线 12V 第 4 组</summary>
|
||||
IS016750_STARTINGPROFILE_12V_4,
|
||||
/// <summary>ISO16750 启动曲线 24V 第 1 组</summary>
|
||||
IS016750_STARTINGPROFILE_24V_1,
|
||||
/// <summary>ISO16750 启动曲线 24V 第 2 组</summary>
|
||||
IS016750_STARTINGPROFILE_24V_2,
|
||||
/// <summary>ISO16750 启动曲线 24V 第 3 组</summary>
|
||||
IS016750_STARTINGPROFILE_24V_3,
|
||||
/// <summary>ISO16750 抛负载测试 A 12V</summary>
|
||||
IS016750_LOADDUMP_TESTA_12V,
|
||||
/// <summary>ISO16750 抛负载测试 A 24V</summary>
|
||||
IS016750_LOADDUMP_TESTA_24V,
|
||||
/// <summary>ISO16750 抛负载测试 B 12V</summary>
|
||||
IS016750_LOADDUMP_TESTB_12V,
|
||||
/// <summary>ISO16750 抛负载测试 B 24V</summary>
|
||||
IS016750_LOADDUMP_TESTB_24V,
|
||||
/// <summary>LV123 高压曲线 1</summary>
|
||||
LV123_HV_1,
|
||||
/// <summary>LV123 高压曲线 2A</summary>
|
||||
LV123_HV_2A,
|
||||
/// <summary>LV123 高压曲线 2B</summary>
|
||||
LV123_HV_2B,
|
||||
/// <summary>LV123 高压曲线 3</summary>
|
||||
LV123_HV_3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S7200 工作模式枚举 (电源模式 / 负载模式)
|
||||
/// </summary>
|
||||
public enum S7200工作模式_枚举
|
||||
{
|
||||
/// <summary>作为电源输出</summary>
|
||||
电源模式,
|
||||
/// <summary>作为负载吸收</summary>
|
||||
负载模式,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高压直流源载一体机 (适配 IT6000C SCPI指令集)
|
||||
/// 高压直流源载一体机 (适配 IT6000C SCPI 指令集),支持电源输出与负载吸收双向工作
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class S7200 : Tcp
|
||||
@@ -67,115 +121,239 @@ namespace DeviceCommand.Devices
|
||||
// SCPI 指令结束符
|
||||
private const string ScpiDelimiter = "\n";
|
||||
|
||||
#region 设置
|
||||
|
||||
public async Task 设置为远程模式(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化一体机通信参数
|
||||
/// </summary>
|
||||
public S7200(TcpConfig config) : base(config)
|
||||
{
|
||||
await SendAsync($"SYSTem:REMote{ScpiDelimiter}", ct); //[cite: 1]
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||
/// 支持科学计数法(如 2.48E-03),结果保留两位小数。
|
||||
/// </summary>
|
||||
/// <param name="raw">设备返回的原始字符串</param>
|
||||
/// <returns>提取到的数值(保留两位小数),解析失败时返回 0</returns>
|
||||
private static double ExtractDouble(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||
|
||||
// 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头
|
||||
var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||
? Math.Round(val, 2)
|
||||
: 0;
|
||||
}
|
||||
|
||||
#region 设置
|
||||
|
||||
/// <summary>
|
||||
/// 设置仪器为远程控制模式 (SYSTem:REMote)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置为远程模式(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"SYSTem:REMote{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置电源工作模式 (电源/负载)。IT6000C 支持双向无缝切换,无需显式下发源载切换指令
|
||||
/// </summary>
|
||||
/// <param name="开关">工作模式枚举 (电源模式 / 负载模式)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置电源工作模式(S7200工作模式_枚举 开关, CancellationToken ct = default)
|
||||
{
|
||||
// IT6000C 为双向无缝切换,底层硬件无需显式发送源载切换指令
|
||||
// 保留此方法仅为兼容您原有上位机的工步调用流程,直接返回即可
|
||||
// 保留此方法仅为兼容原有上位机的工步调用流程
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置直流源工作模式 (FUNCtion 指令,如 VOLTage / CURRent / SOLar 等)
|
||||
/// </summary>
|
||||
/// <param name="模式">直流源工作模式枚举</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置直流源工作模式(S7200直流源模式_枚举 模式, CancellationToken ct = default)
|
||||
{
|
||||
// 原指令: MODE {模式} -> 现 IT6000C 指令: FUNCtion {模式} (如 VOLTage / CURRent)[cite: 1]
|
||||
// 若枚举 ToString() 后不匹配,建议在此处加 Switch 转换
|
||||
await SendAsync($"FUNCtion {模式}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置负载工作模式。IT6000C 负载控制通过负向边界实现
|
||||
/// </summary>
|
||||
/// <param name="模式">负载工作模式枚举</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置负载工作模式(S7200负载模式_枚举 模式, CancellationToken ct = default)
|
||||
{
|
||||
// IT6000C 负载控制通过负向边界实现。若是想开启纯电阻模式,可调用 SINK:RESistance:STATe ON[cite: 1]
|
||||
// 若想开启纯电阻模式,可调用 SINK:RESistance:STATe ON
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开启或关闭通道输出 (OUTPut 指令)
|
||||
/// </summary>
|
||||
/// <param name="开关">true: 开启输出, false: 关闭输出</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置通道开关(bool 开关, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"OUTPut {(开关 ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"OUTPut {(开关 ? 1 : 0)}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置恒流 (CC) 模式的电流值 (CURRent 指令)
|
||||
/// </summary>
|
||||
/// <param name="电流">电流设定值 (单位: A)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置CC模式电流(double 电流, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"CURRent {电流}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"CURRent {电流}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
public async Task 设置CC电压H(double 电流, CancellationToken ct = default) // 沿用原参数名“电流”,实际是设定电压上限
|
||||
/// <summary>
|
||||
/// 设置恒流 (CC) 模式的电压上限 (VOLTage:LIMit:POSitive 指令)
|
||||
/// </summary>
|
||||
/// <param name="电流">电压上限设定值 (单位: V),沿用原参数名</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置CC电压H(double 电流, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"VOLTage:LIMit:POSitive {电流}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"VOLTage:LIMit:POSitive {电流}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
public async Task 设置CC电压L(double 电流, CancellationToken ct = default) // 沿用原参数名“电流”,实际是设定电压下限
|
||||
/// <summary>
|
||||
/// 设置恒流 (CC) 模式的电压下限 (VOLTage:LIMit:NEGative 指令)
|
||||
/// </summary>
|
||||
/// <param name="电流">电压下限设定值 (单位: V),沿用原参数名</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置CC电压L(double 电流, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"VOLTage:LIMit:NEGative {电流}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"VOLTage:LIMit:NEGative {电流}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置恒压 (CV) 模式的正向电流限制 (CURRent:LIMit:POSitive 指令)
|
||||
/// </summary>
|
||||
/// <param name="电流">正向电流限制值 (单位: A)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置CV正向电流(double 电流, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"CURRent:LIMit:POSitive {电流}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"CURRent:LIMit:POSitive {电流}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置恒压 (CV) 模式的反向电流限制 (CURRent:LIMit:NEGative 指令)
|
||||
/// </summary>
|
||||
/// <param name="电流">反向电流限制值 (单位: A),负载模式需传负值</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置CV反向电流(double 电流, CancellationToken ct = default)
|
||||
{
|
||||
// 提醒: 此处传入的参数如果是正数,建议在外部或此处转换为负值,因为载模式限制需要负数[cite: 1]
|
||||
// 载模式限制需要负数,若传入正数建议在外部转换为负值
|
||||
await SendAsync($"CURRent:LIMit:NEGative {电流}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置正向功率阈值 (POWer:LIMit:POSitive 指令)
|
||||
/// </summary>
|
||||
/// <param name="功率">正向功率阈值 (单位: W)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置正向功率阈值(double 功率, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"POWer:LIMit:POSitive {功率}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"POWer:LIMit:POSitive {功率}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置反向功率阈值 (POWer:LIMit:NEGative 指令)
|
||||
/// </summary>
|
||||
/// <param name="功率">反向功率阈值 (单位: W),必须为负数</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置反向功率阈值(double 功率, CancellationToken ct = default)
|
||||
{
|
||||
// 提醒: 此处传入的参数必须为负数[cite: 1]
|
||||
await SendAsync($"POWer:LIMit:NEGative {功率}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置输出电压值 (VOLTage 指令)
|
||||
/// </summary>
|
||||
/// <param name="电压">电压设定值 (单位: V)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置电压(double 电压, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"VOLTage {电压}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"VOLTage {电压}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除保护状态 (OUTPut:PROTection:CLEar 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 清除保护状态(CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"OUTPut:PROTection:CLEar{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"OUTPut:PROTection:CLEar{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开启或关闭过流保护 (OCP) 功能 (CURRent:PROTection:STATe 指令)
|
||||
/// </summary>
|
||||
/// <param name="v">true: 开启过流保护, false: 关闭过流保护</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置OCP开关(bool v, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"CURRent:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"CURRent:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开启或关闭过压保护 (OVP) 功能 (VOLTage:PROTection:STATe 指令)
|
||||
/// </summary>
|
||||
/// <param name="v">true: 开启过压保护, false: 关闭过压保护</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置OVP开关(bool v, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"VOLTage:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"VOLTage:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置过压保护 (OVP) 的触发电压值 (VOLTage:PROTection 指令)
|
||||
/// </summary>
|
||||
/// <param name="oVP电压">过压保护触发电压值 (单位: V)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置电压保护OVP电压(double oVP电压, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"VOLTage:PROTection {oVP电压}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"VOLTage:PROTection {oVP电压}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置过流保护 (OCP) 的触发电流值 (CURRent:PROTection 指令)
|
||||
/// </summary>
|
||||
/// <param name="oCP电流">过流保护触发电流值 (单位: A)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置电流保护OCP电流(double oCP电流, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"CURRent:PROTection {oCP电流}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"CURRent:PROTection {oCP电流}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开启或关闭过功率保护 (OPP) 功能 (POWer:PROTection:STATe 指令)
|
||||
/// </summary>
|
||||
/// <param name="v">true: 开启过功率保护, false: 关闭过功率保护</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置功率保护开关(bool v, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"POWer:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"POWer:PROTection:STATe {(v ? 1 : 0)}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置过功率保护 (OPP) 的触发功率值 (POWer:PROTection 指令)
|
||||
/// </summary>
|
||||
/// <param name="oPP功率">过功率保护触发功率值 (单位: W)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 设置功率保护功率(double oPP功率, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"POWer:PROTection {oPP功率}{ScpiDelimiter}", ct); //[cite: 1]
|
||||
await SendAsync($"POWer:PROTection {oPP功率}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送自定义 SCPI 指令
|
||||
/// </summary>
|
||||
/// <param name="指令">自定义指令字符串</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public async Task 发送自定义命令(string 指令, CancellationToken ct = default)
|
||||
{
|
||||
await SendAsync($"{指令}{ScpiDelimiter}", ct);
|
||||
@@ -185,29 +363,49 @@ namespace DeviceCommand.Devices
|
||||
|
||||
#region 查询
|
||||
|
||||
/// <summary>
|
||||
/// 查询设备识别信息 (*IDN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>设备标识字符串 (制造商, 型号, 序列号, 固件版本)</returns>
|
||||
public async Task<string> 查询设备信息(CancellationToken ct = default)
|
||||
{
|
||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询实时输出电压 (MEASure:VOLTage? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>实时电压值 (单位: V,保留两位小数)</returns>
|
||||
public async Task<double> 查询实时电压(CancellationToken ct = default)
|
||||
{
|
||||
var str = await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||
return double.TryParse(str, out double result) ? result : 0;
|
||||
var str = await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(str);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询实时输出电流 (MEASure:CURRent? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>实时电流值 (单位: A,保留两位小数)</returns>
|
||||
public async Task<double> 查询实时电流(CancellationToken ct = default)
|
||||
{
|
||||
var str = await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||
return double.TryParse(str, out double result) ? result : 0;
|
||||
var str = await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(str);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询实时功率 (MEASure:POWer? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>实时功率值 (单位: W,保留两位小数)</returns>
|
||||
public async Task<double> 查询功率(CancellationToken ct = default)
|
||||
{
|
||||
var str = await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||
return double.TryParse(str, out double result) ? result : 0;
|
||||
var str = await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(str);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -37,43 +38,102 @@ namespace DeviceCommand.Devices
|
||||
/// </summary>
|
||||
public enum TekMeasurementType
|
||||
{
|
||||
/// <summary>幅度</summary>
|
||||
AMPLitude,
|
||||
/// <summary>频率</summary>
|
||||
FREQuency,
|
||||
/// <summary>平均值</summary>
|
||||
MEAN,
|
||||
/// <summary>峰峰值</summary>
|
||||
PK2PK,
|
||||
/// <summary>最大值</summary>
|
||||
MAXimum,
|
||||
/// <summary>最小值</summary>
|
||||
MINimum
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tektronix 示波器 (MSO 4/5/6 系列),支持采集控制、通道设置、自动测量与截图导出
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class TektronixMSO : Tcp
|
||||
{
|
||||
// SCPI 指令结束符 (消息终止符需使用 LF)
|
||||
private const string ScpiDelimiter = "\n";
|
||||
|
||||
/// <summary>
|
||||
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||
/// 支持科学计数法(如 2.48E-03),结果保留两位小数。
|
||||
/// </summary>
|
||||
/// <param name="raw">设备返回的原始字符串</param>
|
||||
/// <returns>提取到的数值(保留两位小数),解析失败时返回 0</returns>
|
||||
private static double ExtractDouble(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||
|
||||
// 从响应中提取第一个数值(含正负号、小数点、科学计数法),可自动忽略单位后缀与命令头
|
||||
var match = Regex.Match(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||
? Math.Round(val, 2)
|
||||
: 0;
|
||||
}
|
||||
|
||||
#region 兼容旧 MSO44B 枚举(供 ACP 测试项反序列化)
|
||||
|
||||
public enum AcquireMode { Sample = 0, PeakDetect = 1, HiRes = 2, Average = 3, Envelope = 4 }
|
||||
public enum BandwidthLimit { Full = 0, BW20MHz = 1 }
|
||||
public enum Channel { Channel1 = 1, Channel2 = 2, Channel3 = 3, Channel4 = 4 }
|
||||
public enum CouplingMode { DC = 0, AC = 1, GND = 2 }
|
||||
public enum MeasurementType
|
||||
{
|
||||
Amplitude = 1, Frequency = 2, Mean = 3, PK2PK = 4,
|
||||
Maximum = 5, Minimum = 6, RMS = 7, Overshoot = 8,
|
||||
Undershoot = 9, PositiveOvershoot = 10, NegativeOvershoot = 11,
|
||||
RiseTime = 12, FallTime = 13, DutyCycle = 14,
|
||||
Period = 15, Phase = 16, Delay = 17,
|
||||
BurstWidth = 18, SlewRateRising = 19, SlewRateFalling = 20,
|
||||
High = 21
|
||||
}
|
||||
public enum StopAfter { Sequence = 0, RunStop = 1 }
|
||||
public enum TriggerMode { Auto = 0, Normal = 1 }
|
||||
public enum TriggerSlope { Rising = 0, Falling = 1 }
|
||||
|
||||
#endregion
|
||||
|
||||
public TektronixMSO(TcpConfig config) : base(config)
|
||||
{
|
||||
}
|
||||
|
||||
#region 1. 公共命令与状态查询
|
||||
|
||||
/// <summary>
|
||||
/// 查询仪器识别码 (*IDN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>设备标识字符串 (制造商, 型号, 序列号, 固件版本)</returns>
|
||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||
{
|
||||
// 返回仪器的标识代码[cite: 2]
|
||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询仪器是否处于忙碌状态,常用于同步操作 (BUSY? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>忙碌状态字符串 (0: 空闲, 1: 忙碌)</returns>
|
||||
public virtual async Task<string> 查询忙碌状态(CancellationToken ct = default)
|
||||
{
|
||||
// 查询仪器是否处于忙碌状态,常用于同步操作[cite: 2]
|
||||
return await WriteReadAsync($"BUSY?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示仪器执行信号路径校准 (SPC) (*CAL? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 执行自校准(CancellationToken ct = default)
|
||||
{
|
||||
// 指示仪器执行信号路径校准 (SPC)[cite: 2]
|
||||
await SendAsync($"*CAL?{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
@@ -82,24 +142,33 @@ namespace DeviceCommand.Devices
|
||||
#region 2. 采集系统控制 (Acquisition)
|
||||
|
||||
/// <summary>
|
||||
/// 启动或停止采集
|
||||
/// 启动或停止采集 (ACQuire:STATE 指令)
|
||||
/// </summary>
|
||||
/// <param name="运行">true: 启动采集 (RUN), false: 停止采集 (STOP)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置采集状态(bool 运行, CancellationToken ct = default)
|
||||
{
|
||||
// 启动、停止或返回采集状态[cite: 2]
|
||||
string 参数 = 运行 ? "RUN" : "STOP";
|
||||
await SendAsync($"ACQuire:STATE {参数}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询当前采集状态 (ACQuire:STATE? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>采集状态字符串 (RUN / STOP)</returns>
|
||||
public virtual async Task<string> 查询采集状态(CancellationToken ct = default)
|
||||
{
|
||||
// 启动、停止或返回采集状态[cite: 2]
|
||||
return await WriteReadAsync($"ACQuire:STATE?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置采集模式 (ACQuire:MODe 指令)
|
||||
/// </summary>
|
||||
/// <param name="模式">采集模式枚举 (采样/峰值检测/高分辨率/平均/包络)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置采集模式(TekAcquisitionMode 模式, CancellationToken ct = default)
|
||||
{
|
||||
// 设置或查询采集模式[cite: 2]
|
||||
await SendAsync($"ACQuire:MODe {模式}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
@@ -107,23 +176,36 @@ namespace DeviceCommand.Devices
|
||||
|
||||
#region 3. 水平系统控制 (Horizontal)
|
||||
|
||||
/// <summary>
|
||||
/// 设置水平刻度 (Scale),即每格代表的时间 (HORizontal:SCAle 指令)
|
||||
/// </summary>
|
||||
/// <param name="秒每格">水平刻度值 (单位: s/Div)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置水平刻度(double 秒每格, CancellationToken ct = default)
|
||||
{
|
||||
// 设置或查询水平刻度 (Scale)[cite: 2]
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, "HORizontal:SCAle {0:E}{1}", 秒每格, ScpiDelimiter);
|
||||
await SendAsync(cmd, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置水平记录长度 (HORizontal:RECOrdlength 指令)
|
||||
/// </summary>
|
||||
/// <param name="长度">记录长度(采样点数)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置记录长度(long 长度, CancellationToken ct = default)
|
||||
{
|
||||
// 设置或查询水平记录长度[cite: 2]
|
||||
await SendAsync($"HORizontal:RECOrdlength {长度}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询采样率(CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询水平采样率 (HORizontal:SAMPLERate? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>水平采样率 (单位: Sa/s,保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询采样率(CancellationToken ct = default)
|
||||
{
|
||||
// 查询水平采样率[cite: 2]
|
||||
return await WriteReadAsync($"HORizontal:SAMPLERate?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($"HORizontal:SAMPLERate?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -131,18 +213,23 @@ namespace DeviceCommand.Devices
|
||||
#region 4. 通道与显示控制 (Vertical)
|
||||
|
||||
/// <summary>
|
||||
/// 全局启用或关闭某通道的显示
|
||||
/// 全局启用或关闭某通道的显示 (DISplay:GLObal:CH:STATE 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号 (1 - 4)</param>
|
||||
/// <param name="开启">true: 开启通道显示, false: 关闭通道显示</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置通道显示开关(int 通道号, bool 开启, CancellationToken ct = default)
|
||||
{
|
||||
// 4/5/6 Series MSO 标准通道显示指令[cite: 2]
|
||||
string 参数 = 开启 ? "ON" : "OFF";
|
||||
await SendAsync($"DISplay:GLObal:CH{通道号}:STATE {参数}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定通道的垂直缩放比例 (Volts/Div)
|
||||
/// 设置指定通道的垂直缩放比例 (CH:SCAle 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号 (1 - 4)</param>
|
||||
/// <param name="伏特每格">垂直刻度值 (单位: V/Div)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置通道垂直刻度(int 通道号, double 伏特每格, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, "CH{0}:SCAle {1:E}{2}", 通道号, 伏特每格, ScpiDelimiter);
|
||||
@@ -150,8 +237,11 @@ namespace DeviceCommand.Devices
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定通道的垂直位置 (Divisions)
|
||||
/// 设置指定通道的垂直位置 (CH:POSition 指令)
|
||||
/// </summary>
|
||||
/// <param name="通道号">通道编号 (1 - 4)</param>
|
||||
/// <param name="格数">垂直位置偏移 (单位: Div)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置通道垂直位置(int 通道号, double 格数, CancellationToken ct = default)
|
||||
{
|
||||
string cmd = string.Format(CultureInfo.InvariantCulture, "CH{0}:POSition {1:F2}{2}", 通道号, 格数, ScpiDelimiter);
|
||||
@@ -163,43 +253,73 @@ namespace DeviceCommand.Devices
|
||||
#region 5. 自动测量 (Measurement)
|
||||
|
||||
/// <summary>
|
||||
/// 动态添加并开启一个测量项
|
||||
/// 动态添加并开启一个测量项 (MEASUrement:ADDNew 指令)。
|
||||
/// 4/5/6 Series MSO 必须先动态创建测量项
|
||||
/// </summary>
|
||||
/// <param name="测量项编号">测量项编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 开启测量项(int 测量项编号, CancellationToken ct = default)
|
||||
{
|
||||
// 4/5/6 Series MSO 必须先动态创建测量项[cite: 2]
|
||||
await SendAsync($"MEASUrement:ADDNew \"MEAS{测量项编号}\"{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置测量项的测量类型 (MEASUrement:TYPe 指令)
|
||||
/// </summary>
|
||||
/// <param name="测量项编号">测量项编号</param>
|
||||
/// <param name="类型">测量类型枚举 (幅度/频率/平均值/峰峰值等)</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置测量类型(int 测量项编号, TekMeasurementType 类型, CancellationToken ct = default)
|
||||
{
|
||||
// 设置或查询测量类型[cite: 2]
|
||||
await SendAsync($"MEASUrement:MEAS{测量项编号}:TYPe {类型}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置测量项的测量源 (MEASUrement:SOUrce1 指令)
|
||||
/// </summary>
|
||||
/// <param name="测量项编号">测量项编号</param>
|
||||
/// <param name="源名称">测量源名称 (如 "CH1", "CH2")</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 设置测量源(int 测量项编号, string 源名称, CancellationToken ct = default)
|
||||
{
|
||||
// 4/5/6 Series 中设置测量源[cite: 2]
|
||||
await SendAsync($"MEASUrement:MEAS{测量项编号}:SOUrce1 {源名称}{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询测量项的当前采集平均值 (MEASUrement:RESUlts:CURRentacq:MEAN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="测量项编号">测量项编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>测量项当前采集的平均值 (保留两位小数)</returns>
|
||||
[Monitorable("测量项的当前采集平均值")]
|
||||
public virtual async Task<string> 查询测量项当前值(int 测量项编号, CancellationToken ct = default)
|
||||
public virtual async Task<double> 查询测量项当前值(int 测量项编号, CancellationToken ct = default)
|
||||
{
|
||||
// 4/5/6 Series 获取当前采集值的标准方式,查询平均值[cite: 2]
|
||||
return await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:CURRentacq:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:CURRentacq:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询测量项全局平均值(int 测量项编号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询测量项的全局历史累积平均值 (MEASUrement:RESUlts:ALLAcqs:MEAN? 指令)
|
||||
/// </summary>
|
||||
/// <param name="测量项编号">测量项编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>测量项全局平均值 (保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询测量项全局平均值(int 测量项编号, CancellationToken ct = default)
|
||||
{
|
||||
// 获取所有采集(AllAcqs)历史累积的平均值[cite: 2]
|
||||
return await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MEAN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
public virtual async Task<string> 查询测量项全局最大值(int 测量项编号, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 查询测量项的全局历史累积最大值 (MEASUrement:RESUlts:ALLAcqs:MAXimum? 指令)
|
||||
/// </summary>
|
||||
/// <param name="测量项编号">测量项编号</param>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>测量项全局最大值 (保留两位小数)</returns>
|
||||
public virtual async Task<double> 查询测量项全局最大值(int 测量项编号, CancellationToken ct = default)
|
||||
{
|
||||
// 获取所有采集(AllAcqs)历史累积的最大值[cite: 2]
|
||||
return await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
string resp = await WriteReadAsync($"MEASUrement:MEAS{测量项编号}:RESUlts:ALLAcqs:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
return ExtractDouble(resp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -215,13 +335,13 @@ namespace DeviceCommand.Devices
|
||||
{
|
||||
string 临时文件 = "E:/temp1.png";
|
||||
|
||||
// 1. 直接保存截图到示波器U盘 (自动识别 .png 扩展名)[cite: 2]
|
||||
// 1. 直接保存截图到示波器U盘 (自动识别 .png 扩展名)
|
||||
await SendAsync($"SAVe:IMAGe \"{临时文件}\"{ScpiDelimiter}", ct);
|
||||
|
||||
// 2. 发送 *OPC? 并等待返回 1,确保图像文件已经完全写入磁盘[cite: 2]
|
||||
// 2. 发送 *OPC? 并等待返回 1,确保图像文件已经完全写入磁盘
|
||||
await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
|
||||
// 3. 将文件内容读取到当前通信接口[cite: 2]
|
||||
// 3. 将文件内容读取到当前通信接口
|
||||
await SendAsync($"FILESystem:READFile \"{临时文件}\"{ScpiDelimiter}", ct);
|
||||
|
||||
// 4. 提取底层的二进制块 (Block Data)
|
||||
@@ -230,7 +350,7 @@ namespace DeviceCommand.Devices
|
||||
// 5. 将提取的二进制字节流写入上位机本地磁盘
|
||||
await File.WriteAllBytesAsync(上位机文件路径, imageBytes, ct);
|
||||
|
||||
// 6. 删除示波器上的临时文件,保持设备存储干净[cite: 2]
|
||||
// 6. 删除示波器上的临时文件,保持设备存储干净
|
||||
await SendAsync($"FILESystem:DELEte \"{临时文件}\"{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
@@ -309,41 +429,44 @@ namespace DeviceCommand.Devices
|
||||
#region 7. 状态与错误队列 (Status and Error)
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有的状态寄存器和错误/事件队列
|
||||
/// 清除所有的状态寄存器和错误/事件队列 (*CLS 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
public virtual async Task 清除错误队列Async(CancellationToken ct = default)
|
||||
{
|
||||
// 发送 *CLS 指令清空状态[cite: 2]
|
||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询错误队列中当前的事件/错误数量
|
||||
/// 查询错误队列中当前的事件/错误数量 (EVQty? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>事件队列中的事件数量字符串</returns>
|
||||
public virtual async Task<string> 查询错误数量Async(CancellationToken ct = default)
|
||||
{
|
||||
// 返回事件队列中的事件数量
|
||||
return await WriteReadAsync($"EVQty?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从错误队列中读取最早的一个事件/错误(包含代码和详细消息),并将其从队列中弹出
|
||||
/// 从错误队列中读取最早的一个事件/错误(包含代码和详细消息),并将其从队列中弹出 (EVMsg? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>事件代码和消息字符串</returns>
|
||||
public virtual async Task<string> 读取单条错误消息Async(CancellationToken ct = default)
|
||||
{
|
||||
// 返回事件队列中的事件代码和消息
|
||||
return await WriteReadAsync($"EVMsg?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取并清空队列中的所有错误和事件消息
|
||||
/// 读取并清空队列中的所有错误和事件消息 (ALLEv? 指令)
|
||||
/// </summary>
|
||||
/// <param name="ct">取消令牌</param>
|
||||
/// <returns>所有事件及其消息字符串</returns>
|
||||
public virtual async Task<string> 读取所有错误消息Async(CancellationToken ct = default)
|
||||
{
|
||||
// 返回所有事件及其消息
|
||||
return await WriteReadAsync($"ALLEv?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,31 @@
|
||||
using NModbus;
|
||||
using NModbus.Serial;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Flexible
|
||||
{
|
||||
/// <summary>
|
||||
/// 灵活型 Modbus RTU 串口通信类(免实例化,每次调用临时创建串口连接)。
|
||||
/// 支持保持寄存器与线圈的读写操作,内部使用按串口名称粒度的通信锁保证同一串口同一时刻只有一个事务在执行,
|
||||
/// 不同串口之间互不阻塞,适用于偶发性、无需保持长连接的 Modbus RTU 设备读写场景。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public static class FModbusRTU
|
||||
{
|
||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
||||
// 按串口名称粒度的通信锁:同一串口同一时刻只有一个事务在执行,不同串口互不阻塞
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||
|
||||
/// <summary>获取指定串口的通信锁(不存在则自动创建)</summary>
|
||||
private static SemaphoreSlim GetLock(string portName)
|
||||
=> _commLocks.GetOrAdd(portName, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
/// <summary>
|
||||
/// 创建串口实例并配置超时参数。
|
||||
/// </summary>
|
||||
private static SerialPort CreatePort(
|
||||
string portName,
|
||||
int baudRate,
|
||||
@@ -29,6 +43,9 @@ namespace DeviceCommand.Flexible
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于指定串口创建 Modbus RTU 主站。
|
||||
/// </summary>
|
||||
private static IModbusMaster CreateMaster(SerialPort port)
|
||||
{
|
||||
return new ModbusFactory().CreateRtuMaster(port);
|
||||
@@ -36,7 +53,22 @@ namespace DeviceCommand.Flexible
|
||||
|
||||
#region Holding Register
|
||||
|
||||
public static async Task<ushort[]> ReadHoldingRegistersAsync(
|
||||
/// <summary>
|
||||
/// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||
/// <param name="startAddress">起始寄存器地址</param>
|
||||
/// <param name="numberOfPoints">读取的寄存器数量</param>
|
||||
/// <param name="dataBits">数据位,默认 8</param>
|
||||
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||
/// <param name="parity">校验位,默认无校验</param>
|
||||
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>读取到的寄存器值数组</returns>
|
||||
public static async Task<ushort[]> 读保持寄存器(
|
||||
string portName,
|
||||
int baudRate,
|
||||
byte slaveAddress,
|
||||
@@ -49,7 +81,7 @@ namespace DeviceCommand.Flexible
|
||||
int writeTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(portName).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var port = CreatePort(
|
||||
@@ -69,11 +101,25 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(portName).Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteSingleRegisterAsync(
|
||||
/// <summary>
|
||||
/// 写入单个保持寄存器(功能码 06)。
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||
/// <param name="registerAddress">要写入的寄存器地址</param>
|
||||
/// <param name="value">要写入的寄存器值 (0-65535)</param>
|
||||
/// <param name="dataBits">数据位,默认 8</param>
|
||||
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||
/// <param name="parity">校验位,默认无校验</param>
|
||||
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 写单个寄存器(
|
||||
string portName,
|
||||
int baudRate,
|
||||
byte slaveAddress,
|
||||
@@ -86,7 +132,7 @@ namespace DeviceCommand.Flexible
|
||||
int writeTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(portName).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var port = CreatePort(
|
||||
@@ -106,7 +152,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(portName).Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +160,22 @@ namespace DeviceCommand.Flexible
|
||||
|
||||
#region Coil
|
||||
|
||||
public static async Task<bool[]> ReadCoilsAsync(
|
||||
/// <summary>
|
||||
/// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||
/// <param name="startAddress">起始线圈地址</param>
|
||||
/// <param name="numberOfPoints">读取的线圈数量</param>
|
||||
/// <param name="dataBits">数据位,默认 8</param>
|
||||
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||
/// <param name="parity">校验位,默认无校验</param>
|
||||
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>线圈状态数组,true 表示吸合/导通</returns>
|
||||
public static async Task<bool[]> 读线圈(
|
||||
string portName,
|
||||
int baudRate,
|
||||
byte slaveAddress,
|
||||
@@ -127,7 +188,7 @@ namespace DeviceCommand.Flexible
|
||||
int writeTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(portName).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var port = CreatePort(
|
||||
@@ -147,11 +208,25 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(portName).Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteSingleCoilAsync(
|
||||
/// <summary>
|
||||
/// 写入单个线圈(功能码 05),控制单个开关量输出。
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||
/// <param name="coilAddress">要写入的线圈地址</param>
|
||||
/// <param name="value">线圈状态(true: 吸合/导通, false: 断开)</param>
|
||||
/// <param name="dataBits">数据位,默认 8</param>
|
||||
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||
/// <param name="parity">校验位,默认无校验</param>
|
||||
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 写单个线圈(
|
||||
string portName,
|
||||
int baudRate,
|
||||
byte slaveAddress,
|
||||
@@ -164,7 +239,7 @@ namespace DeviceCommand.Flexible
|
||||
int writeTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(portName).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var port = CreatePort(
|
||||
@@ -184,7 +259,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(portName).Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Common.Attributes;
|
||||
using NModbus;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -8,11 +9,24 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Flexible
|
||||
{
|
||||
/// <summary>
|
||||
/// 灵活型 Modbus TCP 通信类(免实例化,每次调用临时建立 TCP 连接)。
|
||||
/// 支持保持寄存器与线圈的读写操作,内部使用按端点粒度的通信锁保证同一端点同一时刻只有一个事务在执行,
|
||||
/// 不同端点之间互不阻塞,适用于偶发性、无需保持长连接的 Modbus TCP 设备读写场景。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public static class FModbusTCP
|
||||
{
|
||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
||||
// 按端点粒度的通信锁:同一端点同一时刻只有一个事务在执行,不同端点互不阻塞
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||
|
||||
/// <summary>获取指定端点的通信锁(不存在则自动创建)</summary>
|
||||
private static SemaphoreSlim GetLock(string ipAddress, int port)
|
||||
=> _commLocks.GetOrAdd($"{ipAddress}:{port}", _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
/// <summary>
|
||||
/// 建立 TCP 连接并创建 Modbus TCP 主站。
|
||||
/// </summary>
|
||||
private static async Task<IModbusMaster> ConnectAsync(string ipAddress, int port, int sendTimeout, int receiveTimeout, CancellationToken ct)
|
||||
{
|
||||
var tcpClient = new TcpClient();
|
||||
@@ -26,7 +40,19 @@ namespace DeviceCommand.Flexible
|
||||
|
||||
#region Holding Registers
|
||||
|
||||
public static async Task<ushort[]> ReadHoldingRegistersAsync(
|
||||
/// <summary>
|
||||
/// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||
/// <param name="startAddress">起始寄存器地址</param>
|
||||
/// <param name="numberOfPoints">读取的寄存器数量</param>
|
||||
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>读取到的寄存器值数组</returns>
|
||||
public static async Task<ushort[]> 读保持寄存器(
|
||||
string ipAddress,
|
||||
int port,
|
||||
byte slaveAddress,
|
||||
@@ -36,7 +62,7 @@ namespace DeviceCommand.Flexible
|
||||
int receiveTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||
@@ -47,11 +73,22 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteSingleRegisterAsync(
|
||||
/// <summary>
|
||||
/// 写入单个保持寄存器(功能码 06)。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||
/// <param name="registerAddress">要写入的寄存器地址</param>
|
||||
/// <param name="value">要写入的寄存器值 (0-65535)</param>
|
||||
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 写单个寄存器(
|
||||
string ipAddress,
|
||||
int port,
|
||||
byte slaveAddress,
|
||||
@@ -61,7 +98,7 @@ namespace DeviceCommand.Flexible
|
||||
int receiveTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||
@@ -71,7 +108,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +116,19 @@ namespace DeviceCommand.Flexible
|
||||
|
||||
#region Coils
|
||||
|
||||
public static async Task<bool[]> ReadCoilsAsync(
|
||||
/// <summary>
|
||||
/// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||
/// <param name="startAddress">起始线圈地址</param>
|
||||
/// <param name="numberOfPoints">读取的线圈数量</param>
|
||||
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>线圈状态数组,true 表示吸合/导通</returns>
|
||||
public static async Task<bool[]> 读线圈(
|
||||
string ipAddress,
|
||||
int port,
|
||||
byte slaveAddress,
|
||||
@@ -89,7 +138,7 @@ namespace DeviceCommand.Flexible
|
||||
int receiveTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||
@@ -100,11 +149,22 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteSingleCoilAsync(
|
||||
/// <summary>
|
||||
/// 写入单个线圈(功能码 05),控制单个开关量输出。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||
/// <param name="coilAddress">要写入的线圈地址</param>
|
||||
/// <param name="value">线圈状态(true: 吸合/导通, false: 断开)</param>
|
||||
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 写单个线圈(
|
||||
string ipAddress,
|
||||
int port,
|
||||
byte slaveAddress,
|
||||
@@ -114,7 +174,7 @@ namespace DeviceCommand.Flexible
|
||||
int receiveTimeout = 3000,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||
@@ -124,7 +184,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Common.Attributes;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO.Ports;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -7,11 +8,25 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace DeviceCommand.Flexible
|
||||
{
|
||||
/// <summary>
|
||||
/// 灵活型串口通信类(免实例化,每次调用临时创建串口连接)。
|
||||
/// 支持只发送指令、发送并读取应答两种最常用操作,
|
||||
/// 内部使用按串口名称粒度的通信锁保证同一串口同一时刻只有一个事务在执行,
|
||||
/// 不同串口之间互不阻塞,适用于偶发性、无需保持长连接的串口设备通信场景(如示波器、电源的 SCPI 指令)。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public static class FSerialPort
|
||||
{
|
||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
||||
// 按串口名称粒度的通信锁:同一串口同一时刻只有一个事务在执行,不同串口互不阻塞
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||
|
||||
/// <summary>获取指定串口的通信锁(不存在则自动创建)</summary>
|
||||
private static SemaphoreSlim GetLock(string portName)
|
||||
=> _commLocks.GetOrAdd(portName, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
/// <summary>
|
||||
/// 创建串口实例并配置 UTF8 编码与超时参数。
|
||||
/// </summary>
|
||||
private static SerialPort CreatePort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, int sendTimeout,int receiveTimeout)
|
||||
{
|
||||
return new SerialPort(portName, baudRate, parity, dataBits, stopBits)
|
||||
@@ -24,9 +39,21 @@ namespace DeviceCommand.Flexible
|
||||
|
||||
#region 最常用:只发送字符串
|
||||
|
||||
public static async Task SendAsync(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits,int sendTimeout,int receiveTimeout,string command,CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 向串口发送一条字符串指令(只发送,不等待应答)。
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||
/// <param name="parity">校验位</param>
|
||||
/// <param name="dataBits">数据位,常用 8</param>
|
||||
/// <param name="stopBits">停止位</param>
|
||||
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒)</param>
|
||||
/// <param name="command">要发送的指令字符串</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 发送指令(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits,int sendTimeout,int receiveTimeout,string command,CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(portName).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var port = CreatePort(
|
||||
@@ -44,7 +71,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(portName).Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +79,23 @@ namespace DeviceCommand.Flexible
|
||||
|
||||
#region 最常用:发送字符串并读取字符串
|
||||
|
||||
public static async Task<string> SendReadAsync(string portName,int baudRate, Parity parity, int dataBits, StopBits stopBits,int sendTimeout, int receiveTimeout,string command,string delimiter = "\n", CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 向串口发送一条字符串指令并读取应答,读取到结束符为止(应答不含结束符)。
|
||||
/// </summary>
|
||||
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||
/// <param name="parity">校验位</param>
|
||||
/// <param name="dataBits">数据位,常用 8</param>
|
||||
/// <param name="stopBits">停止位</param>
|
||||
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒),超时未收到结束符抛出超时异常</param>
|
||||
/// <param name="command">要发送的指令字符串</param>
|
||||
/// <param name="delimiter">应答结束符,默认换行符 "\n"</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>去除结束符并去除首尾空白后的应答字符串</returns>
|
||||
public static async Task<string> 发送并读取应答(string portName,int baudRate, Parity parity, int dataBits, StopBits stopBits,int sendTimeout, int receiveTimeout,string command,string delimiter = "\n", CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(portName).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var port = CreatePort(
|
||||
@@ -95,7 +136,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(portName).Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
using Common.Attributes;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace DeviceCommand.Flexible
|
||||
{
|
||||
/// <summary>
|
||||
/// 灵活型 TCP 通信类(免实例化,每次调用临时建立 TCP 连接)。
|
||||
/// 支持字节/文本发送、定长字节读取、按结束符读取文本行四种操作,
|
||||
/// 内部使用按端点粒度的通信锁保证同一端点同一时刻只有一个 TCP 事务在执行,
|
||||
/// 不同端点之间互不阻塞,适用于偶发性、无需保持长连接的 TCP 设备通信场景。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public static class FTCP
|
||||
{
|
||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
||||
// 按端点粒度的通信锁:同一端点同一时刻只有一个事务在执行,不同端点互不阻塞
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||
|
||||
/// <summary>获取指定端点的通信锁(不存在则自动创建)</summary>
|
||||
private static SemaphoreSlim GetLock(string ipAddress, int port)
|
||||
=> _commLocks.GetOrAdd($"{ipAddress}:{port}", _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
#region Send
|
||||
|
||||
public static async Task SendAsync(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 向指定 TCP 端点发送一段字节数据(发送完成后立即断开)。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">TCP 端口号</param>
|
||||
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||
/// <param name="buffer">要发送的字节数组</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 发送字节数据(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
@@ -26,22 +46,39 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static Task SendAsync(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 向指定 TCP 端点发送一段文本(以 UTF8 编码为字节后发送,发送完成后立即断开)。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">TCP 端口号</param>
|
||||
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||
/// <param name="text">要发送的文本字符串</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static Task 发送文本数据(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default)
|
||||
{
|
||||
return SendAsync( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct);
|
||||
return 发送字节数据( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read
|
||||
|
||||
public static async Task<byte[]> ReadAsync(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 连接指定 TCP 端点并读取指定长度的字节数据(连接关闭或读满为止)。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">TCP 端口号</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒)</param>
|
||||
/// <param name="length">要读取的字节长度</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>实际读取到的字节数组(对端提前关闭时可能短于请求长度)</returns>
|
||||
public static async Task<byte[]> 读取字节数据(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
@@ -68,13 +105,22 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> ReadLineAsync( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default)
|
||||
/// <summary>
|
||||
/// 连接指定 TCP 端点并读取一行文本,读取到结束符为止(返回内容不含结束符)。
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">设备 IP 地址</param>
|
||||
/// <param name="port">TCP 端口号</param>
|
||||
/// <param name="receiveTimeout">接收超时时间(毫秒),超时未收到结束符抛出超时异常</param>
|
||||
/// <param name="delimiter">文本行结束符,默认换行符 "\n"</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
/// <returns>去除结束符并去除首尾空白后的文本行</returns>
|
||||
public static async Task<string> 读取文本行( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default)
|
||||
{
|
||||
await _commLock.WaitAsync(ct);
|
||||
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
@@ -106,7 +152,7 @@ namespace DeviceCommand.Flexible
|
||||
}
|
||||
finally
|
||||
{
|
||||
_commLock.Release();
|
||||
GetLock(ipAddress, port).Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,22 @@ namespace DeviceEditModule
|
||||
containerRegistry.RegisterDialog<DialogMangerView, DialogMangerViewModel>("DialogMangerView");
|
||||
|
||||
// 设备编辑 View 注册为 Navigation(被动态解析后作为 Tab 内容嵌入 DialogMangerView)
|
||||
//containerRegistry.RegisterForNavigation<IT7800EView>("IT7800EView");
|
||||
//containerRegistry.Register<SPAW7000ViewModel>();
|
||||
containerRegistry.RegisterForNavigation<DG1000ZView>("DG1000ZView");
|
||||
containerRegistry.RegisterForNavigation<IT6720View>("IT6720View");
|
||||
containerRegistry.RegisterForNavigation<PW8001View>("PW8001View");
|
||||
containerRegistry.RegisterForNavigation<ANEVH80View>("ANEVH80View");
|
||||
containerRegistry.RegisterForNavigation<Chroma61800View>("Chroma61800View");
|
||||
containerRegistry.RegisterForNavigation<MCc30WView>("MCc30WView");
|
||||
containerRegistry.RegisterForNavigation<RLT1000View>("RLT1000View");
|
||||
containerRegistry.RegisterForNavigation<S7200View>("S7200View");
|
||||
containerRegistry.Register<DG1000ZViewModel>();
|
||||
containerRegistry.Register<IT6720ViewModel>();
|
||||
containerRegistry.Register<PW8001ViewModel>();
|
||||
containerRegistry.Register<ANEVH80ViewModel>();
|
||||
containerRegistry.Register<Chroma61800ViewModel>();
|
||||
containerRegistry.Register<MCc30WViewModel>();
|
||||
containerRegistry.Register<RLT1000ViewModel>();
|
||||
containerRegistry.Register<S7200ViewModel>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// ANEVH 高压源载一体机 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class ANEVH80ViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
private readonly DeviceManager _dm;
|
||||
private ANEVH80? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private string _deviceName = "ANEVH80";
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数 — 源模式
|
||||
private double _voltage;
|
||||
public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
|
||||
private double _current;
|
||||
public double Current { get => _current; set => SetProperty(ref _current, value); }
|
||||
private double _power;
|
||||
public double Power { get => _power; set => SetProperty(ref _power, value); }
|
||||
#endregion
|
||||
|
||||
#region 输入参数 — 负载(SINK)参数
|
||||
private double _sinkCurrent;
|
||||
public double SinkCurrent { get => _sinkCurrent; set => SetProperty(ref _sinkCurrent, value); }
|
||||
private double _sinkPower;
|
||||
public double SinkPower { get => _sinkPower; set => SetProperty(ref _sinkPower, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _measuredVoltage;
|
||||
public double MeasuredVoltage { get => _measuredVoltage; set => SetProperty(ref _measuredVoltage, value); }
|
||||
private double _measuredCurrent;
|
||||
public double MeasuredCurrent { get => _measuredCurrent; set => SetProperty(ref _measuredCurrent, value); }
|
||||
private double _measuredPower;
|
||||
public double MeasuredPower { get => _measuredPower; set => SetProperty(ref _measuredPower, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand QueryIdn { get; } public ICommand Reset { get; }
|
||||
public ICommand OutOn { get; } public ICommand OutOff { get; }
|
||||
public ICommand SetV { get; } public ICommand SetI { get; } public ICommand SetP { get; }
|
||||
public ICommand SetSinkI { get; } public ICommand SetSinkP { get; }
|
||||
public ICommand QMeas { get; }
|
||||
#endregion
|
||||
|
||||
public ANEVH80ViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
QueryIdn = new DelegateCommand(async () => await Exec(async () => Log("IDN:" + await _dev!.查询设备信息Async(Ct()))));
|
||||
Reset = new DelegateCommand(async () => await Exec(async () => { await _dev!.复位Async(Ct()); Log("设备已复位"); }));
|
||||
OutOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关Async(true, Ct()); Log("输出已开启"); }));
|
||||
OutOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关Async(false, Ct()); Log("输出已关闭"); }));
|
||||
SetV = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置电压Async(Voltage, Ct()); Log($"电压={Voltage}V"); }));
|
||||
SetI = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置电流Async(Current, Ct()); Log($"电流={Current}A"); }));
|
||||
SetP = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置功率Async(Power, Ct()); Log($"功率={Power}W"); }));
|
||||
SetSinkI = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置负载电流Async(SinkCurrent, Ct()); Log($"负载电流={SinkCurrent}A"); }));
|
||||
SetSinkP = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置负载功率Async(SinkPower, Ct()); Log($"负载功率={SinkPower}W"); }));
|
||||
QMeas = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
MeasuredVoltage = await _dev!.读取电压Async(Ct());
|
||||
MeasuredCurrent = await _dev!.读取电流Async(Ct());
|
||||
MeasuredPower = await _dev!.读取功率Async(Ct());
|
||||
Log($"测量→V:{MeasuredVoltage} I:{MeasuredCurrent} P:{MeasuredPower}");
|
||||
}));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#region 初始化 / Navigation
|
||||
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
ANEVH80? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is ANEVH80 e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is ANEVH80 it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "ANEVH80 (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 ANEVH80 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
? line + "\n" + ResponseLog[..3000]
|
||||
: line + "\n" + ResponseLog;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Chroma61800 交流源一拖三 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class Chroma61800ViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
private readonly DeviceManager _dm;
|
||||
private Chroma61800? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private string _deviceName = "Chroma61800";
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private double _voltage; public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
|
||||
private double _frequency = 50; public double Frequency { get => _frequency; set => SetProperty(ref _frequency, value); }
|
||||
private string _waveform = "SINE"; public string Waveform { get => _waveform; set => SetProperty(ref _waveform, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _measuredVoltage;
|
||||
public double MeasuredVoltage { get => _measuredVoltage; set => SetProperty(ref _measuredVoltage, value); }
|
||||
private double _measuredCurrent;
|
||||
public double MeasuredCurrent { get => _measuredCurrent; set => SetProperty(ref _measuredCurrent, value); }
|
||||
private double _measuredFrequency;
|
||||
public double MeasuredFrequency { get => _measuredFrequency; set => SetProperty(ref _measuredFrequency, value); }
|
||||
private double _measuredPower;
|
||||
public double MeasuredPower { get => _measuredPower; set => SetProperty(ref _measuredPower, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand QueryIdn { get; } public ICommand Reset { get; }
|
||||
public ICommand OutOn { get; } public ICommand OutOff { get; }
|
||||
public ICommand SetV { get; } public ICommand SetF { get; } public ICommand SetWave { get; }
|
||||
public ICommand SetThreePhase { get; } public ICommand SetSinglePhase { get; }
|
||||
public ICommand QMeas { get; }
|
||||
#endregion
|
||||
|
||||
public Chroma61800ViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
QueryIdn = new DelegateCommand(async () => await Exec(async () => Log("IDN:" + await _dev!.查询设备信息Async(Ct()))));
|
||||
Reset = new DelegateCommand(async () => await Exec(async () => { await _dev!.复位Async(Ct()); Log("设备已复位"); }));
|
||||
OutOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关Async(true, Ct()); Log("输出已开启"); }));
|
||||
OutOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关Async(false, Ct()); Log("输出已关闭"); }));
|
||||
SetV = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置交流电压Async(Voltage, Ct()); Log($"电压={Voltage}V"); }));
|
||||
SetF = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置频率Async(Frequency, Ct()); Log($"频率={Frequency}Hz"); }));
|
||||
SetWave = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置波形Async(Waveform, Ct()); Log($"波形={Waveform}"); }));
|
||||
SetThreePhase = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置单三相模式Async(true, Ct()); Log("三相模式"); }));
|
||||
SetSinglePhase = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置单三相模式Async(false, Ct()); Log("单相模式"); }));
|
||||
QMeas = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
MeasuredVoltage = await _dev!.读取交流电压Async(Ct());
|
||||
MeasuredCurrent = await _dev!.读取交流电流Async(Ct());
|
||||
MeasuredFrequency = await _dev!.读取频率Async(Ct());
|
||||
MeasuredPower = await _dev!.读取真实功率Async(Ct());
|
||||
Log($"测量→V:{MeasuredVoltage} I:{MeasuredCurrent} F:{MeasuredFrequency} P:{MeasuredPower}");
|
||||
}));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#region 初始化 / Navigation
|
||||
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
Chroma61800? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is Chroma61800 e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is Chroma61800 it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "Chroma61800 (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 Chroma61800 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
? line + "\n" + ResponseLog[..3000]
|
||||
: line + "\n" + ResponseLog;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +1,122 @@
|
||||
using DeviceCommand.Devices;
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// DG1000Z 信号发生器一拖三 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class DG1000ZViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
#region 私有字段
|
||||
private readonly DeviceManager _deviceManager;
|
||||
private DG1000Z? _device;
|
||||
private readonly DeviceManager _dm;
|
||||
private DG1000Z? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
#endregion
|
||||
#region 属性
|
||||
|
||||
private string _deviceName = "DG1000Z";
|
||||
public string DeviceName
|
||||
{
|
||||
get => _deviceName;
|
||||
set => SetProperty(ref _deviceName, value);
|
||||
}
|
||||
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
/// <summary>正在执行设备命令时为 true,用于 UI 忙碌状态指示。</summary>
|
||||
public bool IsBusy
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private int _channel = 1; public int Channel { get => _channel; set => SetProperty(ref _channel, value); }
|
||||
private double _frequency = 1000; public double Frequency { get => _frequency; set => SetProperty(ref _frequency, value); }
|
||||
private double _amplitude = 5; public double Amplitude { get => _amplitude; set => SetProperty(ref _amplitude, value); }
|
||||
private double _offsetVoltage; public double OffsetVoltage { get => _offsetVoltage; set => SetProperty(ref _offsetVoltage, value); }
|
||||
private double _dutyCycle = 50; public double DutyCycle { get => _dutyCycle; set => SetProperty(ref _dutyCycle, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _measuredFrequency;
|
||||
public double MeasuredFrequency { get => _measuredFrequency; set => SetProperty(ref _measuredFrequency, value); }
|
||||
private double _measuredAmplitude;
|
||||
public double MeasuredAmplitude { get => _measuredAmplitude; set => SetProperty(ref _measuredAmplitude, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand QueryIdn { get; } public ICommand Reset { get; }
|
||||
public ICommand OutOn { get; } public ICommand OutOff { get; }
|
||||
public ICommand SetFreq { get; } public ICommand SetAmp { get; } public ICommand SetOffset { get; }
|
||||
public ICommand SetDuty { get; } public ICommand QueryFreq { get; } public ICommand QueryAmp { get; }
|
||||
public ICommand QueryCounter { get; } public ICommand QueryError { get; }
|
||||
#endregion
|
||||
|
||||
public DG1000ZViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
get => _isBusy;
|
||||
set => SetProperty(ref _isBusy, value);
|
||||
}
|
||||
private string _responseLog = string.Empty;
|
||||
/// <summary>命令响应日志(最新消息在顶部)。</summary>
|
||||
public string ResponseLog
|
||||
{
|
||||
get => _responseLog;
|
||||
set => SetProperty(ref _responseLog, value);
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
QueryIdn = new DelegateCommand(async () => await Exec(async () => Log("IDN:" + await _dev!.查询设备标识(Ct()))));
|
||||
Reset = new DelegateCommand(async () => await Exec(async () => { await _dev!.重置设备(Ct()); Log("设备已重置"); }));
|
||||
OutOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置通道输出状态(Channel, true, Ct()); Log($"CH{Channel}输出开"); }));
|
||||
OutOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置通道输出状态(Channel, false, Ct()); Log($"CH{Channel}输出关"); }));
|
||||
SetFreq = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置频率(Channel, Frequency, Ct()); Log($"CH{Channel}频率={Frequency}Hz"); }));
|
||||
SetAmp = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置幅度(Channel, Amplitude, Ct()); Log($"CH{Channel}幅度={Amplitude}Vpp"); }));
|
||||
SetOffset = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置偏移电压(Channel, OffsetVoltage, Ct()); Log($"CH{Channel}偏移={OffsetVoltage}Vdc"); }));
|
||||
SetDuty = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置方波占空比(Channel, DutyCycle, Ct()); Log($"CH{Channel}占空比={DutyCycle}%"); }));
|
||||
QueryFreq = new DelegateCommand(async () => await Exec(async () => { MeasuredFrequency = await _dev!.查询频率(Channel, Ct()); Log($"CH{Channel}频率={MeasuredFrequency}Hz"); }));
|
||||
QueryAmp = new DelegateCommand(async () => await Exec(async () => { MeasuredAmplitude = await _dev!.查询幅度(Channel, Ct()); Log($"CH{Channel}幅度={MeasuredAmplitude}Vpp"); }));
|
||||
QueryCounter = new DelegateCommand(async () => await Exec(async () => { MeasuredFrequency = await _dev!.查询频率计测量值(Ct()); Log($"频率计={MeasuredFrequency}Hz"); }));
|
||||
QueryError = new DelegateCommand(async () => await Exec(async () => Log("错误:" + await _dev!.查询错误信息(Ct()))));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region 命令
|
||||
#endregion
|
||||
public DG1000ZViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
#region 初始化 / Navigation
|
||||
|
||||
/// <summary>
|
||||
/// 从 DeviceManager 中查找DG1000Z设备实例。
|
||||
/// 优先按 <paramref name="deviceName"/> 查找,否则取第一个匹配类型的设备。
|
||||
/// </summary>
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
DG1000Z? found = null;
|
||||
string? foundName = null;
|
||||
if (deviceName != null &&
|
||||
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
|
||||
d is DG1000Z e)
|
||||
{
|
||||
found = e;
|
||||
foundName = deviceName;
|
||||
}
|
||||
DG1000Z? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is DG1000Z e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _deviceManager.DeviceMap)
|
||||
{
|
||||
if (kv.Value is DG1000Z it)
|
||||
{
|
||||
found = it;
|
||||
foundName = kv.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is DG1000Z it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
|
||||
_device = found;
|
||||
DeviceName = foundName ?? "IT7800E (未找到)";
|
||||
IsConnected = _device?.IsConnected ?? false;
|
||||
|
||||
AppendLog(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 IT7800E 设备,请先初始化设备配置。");
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "DG1000Z (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 DG1000Z 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_device == null)
|
||||
{
|
||||
AppendLog("错误:未关联到设备实例,请检查设备配置。");
|
||||
return;
|
||||
}
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _device.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AppendLog("命令超时或已取消。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog($"错误:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
@@ -136,12 +125,11 @@ namespace DeviceEditModule.ViewModels
|
||||
}
|
||||
|
||||
#endregion
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var name = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +1,124 @@
|
||||
using DeviceCommand.Devices;
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// IT6720 低压电源一拖三 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class IT6720ViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
#region 私有字段
|
||||
private readonly DeviceManager _deviceManager;
|
||||
private IT6720? _device;
|
||||
private readonly DeviceManager _dm;
|
||||
private IT6720? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
#endregion
|
||||
#region 属性
|
||||
|
||||
private string _deviceName = "IT6720";
|
||||
public string DeviceName
|
||||
{
|
||||
get => _deviceName;
|
||||
set => SetProperty(ref _deviceName, value);
|
||||
}
|
||||
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
/// <summary>正在执行设备命令时为 true,用于 UI 忙碌状态指示。</summary>
|
||||
public bool IsBusy
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private double _voltage; public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
|
||||
private double _current; public double Current { get => _current; set => SetProperty(ref _current, value); }
|
||||
private double _voltageLimit; public double VoltageLimit { get => _voltageLimit; set => SetProperty(ref _voltageLimit, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _measuredVoltage;
|
||||
public double MeasuredVoltage { get => _measuredVoltage; set => SetProperty(ref _measuredVoltage, value); }
|
||||
private double _measuredCurrent;
|
||||
public double MeasuredCurrent { get => _measuredCurrent; set => SetProperty(ref _measuredCurrent, value); }
|
||||
private string _outputMode = "";
|
||||
public string OutputMode { get => _outputMode; set => SetProperty(ref _outputMode, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand QueryIdn { get; } public ICommand OutOn { get; } public ICommand OutOff { get; }
|
||||
public ICommand SetRemote { get; } public ICommand SetLocal { get; }
|
||||
public ICommand SetV { get; } public ICommand SetI { get; } public ICommand SetVLim { get; }
|
||||
public ICommand QMeas { get; } public ICommand QMode { get; }
|
||||
#endregion
|
||||
|
||||
public IT6720ViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
get => _isBusy;
|
||||
set => SetProperty(ref _isBusy, value);
|
||||
}
|
||||
private string _responseLog = string.Empty;
|
||||
/// <summary>命令响应日志(最新消息在顶部)。</summary>
|
||||
public string ResponseLog
|
||||
{
|
||||
get => _responseLog;
|
||||
set => SetProperty(ref _responseLog, value);
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
QueryIdn = new DelegateCommand(async () => await Exec(async () => Log("IDN:" + await _dev!.查询设备信息(Ct()))));
|
||||
OutOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关(true, Ct()); Log("输出已开启"); }));
|
||||
OutOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出开关(false, Ct()); Log("输出已关闭"); }));
|
||||
SetRemote = new DelegateCommand(async () => await Exec(async () => { await _dev!.切换远程控制模式(true, Ct()); Log("远程控制"); }));
|
||||
SetLocal = new DelegateCommand(async () => await Exec(async () => { await _dev!.切换本地控制模式(Ct()); Log("本地控制"); }));
|
||||
SetV = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出电压(Voltage, Ct()); Log($"电压={Voltage}V"); }));
|
||||
SetI = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置输出电流(Current, Ct()); Log($"电流={Current}A"); }));
|
||||
SetVLim = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置电压上限(VoltageLimit, Ct()); Log($"电压上限={VoltageLimit}V"); }));
|
||||
QMeas = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
MeasuredVoltage = await _dev!.查询实际电压(Ct());
|
||||
MeasuredCurrent = await _dev!.查询实际电流(Ct());
|
||||
Log($"测量→V:{MeasuredVoltage} I:{MeasuredCurrent}");
|
||||
}));
|
||||
QMode = new DelegateCommand(async () => await Exec(async () => { OutputMode = await _dev!.查询输出模式(Ct()); Log($"输出模式={OutputMode}"); }));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region 命令
|
||||
#endregion
|
||||
public IT6720ViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
#region 初始化 / Navigation
|
||||
|
||||
/// <summary>
|
||||
/// 从 DeviceManager 中查找IT6720设备实例。
|
||||
/// 优先按 <paramref name="deviceName"/> 查找,否则取第一个匹配类型的设备。
|
||||
/// </summary>
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
IT6720? found = null;
|
||||
string? foundName = null;
|
||||
if (deviceName != null &&
|
||||
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
|
||||
d is IT6720 e)
|
||||
{
|
||||
found = e;
|
||||
foundName = deviceName;
|
||||
}
|
||||
IT6720? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is IT6720 e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _deviceManager.DeviceMap)
|
||||
{
|
||||
if (kv.Value is IT6720 it)
|
||||
{
|
||||
found = it;
|
||||
foundName = kv.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is IT6720 it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
|
||||
_device = found;
|
||||
DeviceName = foundName ?? "IT7800E (未找到)";
|
||||
IsConnected = _device?.IsConnected ?? false;
|
||||
|
||||
AppendLog(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 IT7800E 设备,请先初始化设备配置。");
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "IT6720 (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 IT6720 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_device == null)
|
||||
{
|
||||
AppendLog("错误:未关联到设备实例,请检查设备配置。");
|
||||
return;
|
||||
}
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _device.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AppendLog("命令超时或已取消。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog($"错误:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
@@ -136,12 +127,11 @@ namespace DeviceEditModule.ViewModels
|
||||
}
|
||||
|
||||
#endregion
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var name = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// MCc30W 水冷机一拖三 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class MCc30WViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
private readonly DeviceManager _dm;
|
||||
private MCc30W? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private string _deviceName = "MCc30W";
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private int _loop = 1; public int Loop { get => _loop; set => SetProperty(ref _loop, value); }
|
||||
private float _tempSet = 25f; public float TempSet { get => _tempSet; set => SetProperty(ref _tempSet, value); }
|
||||
private float _flowSet = 10f; public float FlowSet { get => _flowSet; set => SetProperty(ref _flowSet, value); }
|
||||
private float _pressSet = 100f; public float PressSet { get => _pressSet; set => SetProperty(ref _pressSet, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private float _currentTemp;
|
||||
public float CurrentTemp { get => _currentTemp; set => SetProperty(ref _currentTemp, value); }
|
||||
private float _currentFlow;
|
||||
public float CurrentFlow { get => _currentFlow; set => SetProperty(ref _currentFlow, value); }
|
||||
private float _currentPress;
|
||||
public float CurrentPress { get => _currentPress; set => SetProperty(ref _currentPress, value); }
|
||||
private uint _alarmInfo;
|
||||
public uint AlarmInfo { get => _alarmInfo; set => SetProperty(ref _alarmInfo, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand LoopOn { get; } public ICommand LoopOff { get; }
|
||||
public ICommand SetTemp { get; } public ICommand SetFlow { get; } public ICommand SetPress { get; }
|
||||
public ICommand AlarmReset { get; } public ICommand AlarmSilence { get; }
|
||||
public ICommand ReadAll { get; } public ICommand ReadAlarm { get; }
|
||||
#endregion
|
||||
|
||||
public MCc30WViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
LoopOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置回路运行Async(Loop, true, Ct()); Log($"回路{Loop}已启动"); }));
|
||||
LoopOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置回路运行Async(Loop, false, Ct()); Log($"回路{Loop}已停止"); }));
|
||||
SetTemp = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置出液目标温度Async(Loop, TempSet, Ct()); Log($"回路{Loop}温度={TempSet}℃"); }));
|
||||
SetFlow = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置出液目标流量Async(Loop, FlowSet, Ct()); Log($"回路{Loop}流量={FlowSet}L/min"); }));
|
||||
SetPress = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置出液目标压力Async(Loop, PressSet, Ct()); Log($"回路{Loop}压力={PressSet}kPa"); }));
|
||||
AlarmReset = new DelegateCommand(async () => await Exec(async () => { await _dev!.报警复位Async(true, Ct()); await _dev!.报警复位Async(false, Ct()); Log("报警已复位"); }));
|
||||
AlarmSilence = new DelegateCommand(async () => await Exec(async () => { await _dev!.报警消音Async(true, Ct()); Log("报警已消音"); }));
|
||||
ReadAll = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
CurrentTemp = await _dev!.读取出液当前温度Async(Loop, Ct());
|
||||
CurrentFlow = await _dev!.读取出液当前流量Async(Loop, Ct());
|
||||
CurrentPress = await _dev!.读取出液当前压力Async(Loop, Ct());
|
||||
Log($"回路{Loop}→T:{CurrentTemp}℃ F:{CurrentFlow}L/min P:{CurrentPress}kPa");
|
||||
}));
|
||||
ReadAlarm = new DelegateCommand(async () => await Exec(async () => { AlarmInfo = await _dev!.读报警信息1Async(Ct()); Log($"报警信息={AlarmInfo}"); }));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#region 初始化 / Navigation
|
||||
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
MCc30W? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is MCc30W e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is MCc30W it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "MCc30W (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 MCc30W 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
? line + "\n" + ResponseLog[..3000]
|
||||
: line + "\n" + ResponseLog;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +1,128 @@
|
||||
using DeviceCommand.Device;
|
||||
using DeviceCommand.Devices;
|
||||
using DeviceCommand.Device;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// PW8001 功率分析仪 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class PW8001ViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
#region 私有字段
|
||||
private readonly DeviceManager _deviceManager;
|
||||
private PW8001? _device;
|
||||
private readonly DeviceManager _dm;
|
||||
private PW8001? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
#endregion
|
||||
#region 属性
|
||||
|
||||
private string _deviceName = "PW8001";
|
||||
public string DeviceName
|
||||
{
|
||||
get => _deviceName;
|
||||
set => SetProperty(ref _deviceName, value);
|
||||
}
|
||||
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
/// <summary>正在执行设备命令时为 true,用于 UI 忙碌状态指示。</summary>
|
||||
public bool IsBusy
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private int _channel = 1; public int Channel { get => _channel; set => SetProperty(ref _channel, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _measuredVoltage;
|
||||
public double MeasuredVoltage { get => _measuredVoltage; set => SetProperty(ref _measuredVoltage, value); }
|
||||
private double _measuredCurrent;
|
||||
public double MeasuredCurrent { get => _measuredCurrent; set => SetProperty(ref _measuredCurrent, value); }
|
||||
private double _measuredPower;
|
||||
public double MeasuredPower { get => _measuredPower; set => SetProperty(ref _measuredPower, value); }
|
||||
private double _voltageTHD;
|
||||
public double VoltageTHD { get => _voltageTHD; set => SetProperty(ref _voltageTHD, value); }
|
||||
private double _currentTHD;
|
||||
public double CurrentTHD { get => _currentTHD; set => SetProperty(ref _currentTHD, value); }
|
||||
private double _totalPower;
|
||||
public double TotalPower { get => _totalPower; set => SetProperty(ref _totalPower, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand QueryIdn { get; } public ICommand Reset { get; } public ICommand Clear { get; }
|
||||
public ICommand ModeWIDE { get; } public ICommand ModeIEC { get; }
|
||||
public ICommand QV { get; } public ICommand QI { get; } public ICommand QP { get; }
|
||||
public ICommand QP123 { get; } public ICommand QTHD { get; }
|
||||
#endregion
|
||||
|
||||
public PW8001ViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
get => _isBusy;
|
||||
set => SetProperty(ref _isBusy, value);
|
||||
}
|
||||
private string _responseLog = string.Empty;
|
||||
/// <summary>命令响应日志(最新消息在顶部)。</summary>
|
||||
public string ResponseLog
|
||||
{
|
||||
get => _responseLog;
|
||||
set => SetProperty(ref _responseLog, value);
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
QueryIdn = new DelegateCommand(async () => await Exec(async () => Log("IDN:" + await _dev!.查询机器信息(Ct()))));
|
||||
Reset = new DelegateCommand(async () => await Exec(async () => { await _dev!.复位仪器(Ct()); Log("仪器已复位"); }));
|
||||
Clear = new DelegateCommand(async () => await Exec(async () => { await _dev!.清除状态(Ct()); Log("状态已清除"); }));
|
||||
ModeWIDE = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置测试模式_WIDE(Ct()); Log("WIDE模式"); }));
|
||||
ModeIEC = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置测试模式_IEC(Ct()); Log("IEC模式"); }));
|
||||
QV = new DelegateCommand(async () => await Exec(async () => { MeasuredVoltage = await _dev!.查询电压_不含变比(Channel, Ct()); Log($"CH{Channel}电压={MeasuredVoltage}V"); }));
|
||||
QI = new DelegateCommand(async () => await Exec(async () => { MeasuredCurrent = await _dev!.查询电流_不含变比(Channel, Ct()); Log($"CH{Channel}电流={MeasuredCurrent}A"); }));
|
||||
QP = new DelegateCommand(async () => await Exec(async () => { MeasuredPower = await _dev!.查询功率_不含变比(Channel, Ct()); Log($"CH{Channel}功率={MeasuredPower}W"); }));
|
||||
QP123 = new DelegateCommand(async () => await Exec(async () => { TotalPower = await _dev!.查询总功率P123(Ct()); Log($"三相总功率={TotalPower}W"); }));
|
||||
QTHD = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
VoltageTHD = await _dev!.查询电压THD(Channel, Ct());
|
||||
CurrentTHD = await _dev!.查询电流THD(Channel, Ct());
|
||||
Log($"CH{Channel} THD→U:{VoltageTHD}% I:{CurrentTHD}%");
|
||||
}));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region 命令
|
||||
#endregion
|
||||
public PW8001ViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
#region 初始化 / Navigation
|
||||
|
||||
/// <summary>
|
||||
/// 从 DeviceManager 中查找PW8001设备实例。
|
||||
/// 优先按 <paramref name="deviceName"/> 查找,否则取第一个匹配类型的设备。
|
||||
/// </summary>
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
PW8001? found = null;
|
||||
string? foundName = null;
|
||||
if (deviceName != null &&
|
||||
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
|
||||
d is PW8001 e)
|
||||
{
|
||||
found = e;
|
||||
foundName = deviceName;
|
||||
}
|
||||
PW8001? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is PW8001 e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _deviceManager.DeviceMap)
|
||||
{
|
||||
if (kv.Value is PW8001 it)
|
||||
{
|
||||
found = it;
|
||||
foundName = kv.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is PW8001 it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
|
||||
_device = found;
|
||||
DeviceName = foundName ?? "IT7800E (未找到)";
|
||||
IsConnected = _device?.IsConnected ?? false;
|
||||
|
||||
AppendLog(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 IT7800E 设备,请先初始化设备配置。");
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "PW8001 (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 PW8001 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_device == null)
|
||||
{
|
||||
AppendLog("错误:未关联到设备实例,请检查设备配置。");
|
||||
return;
|
||||
}
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _device.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
AppendLog("命令超时或已取消。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog($"错误:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
@@ -137,12 +131,11 @@ namespace DeviceEditModule.ViewModels
|
||||
}
|
||||
|
||||
#endregion
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var name = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// RLT1000 环境箱 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class RLT1000ViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
private readonly DeviceManager _dm;
|
||||
private RLT1000? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private string _deviceName = "RLT1000";
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private float _targetTemp = 25f; public float TargetTemp { get => _targetTemp; set => SetProperty(ref _targetTemp, value); }
|
||||
private float _targetHumidity = 50f; public float TargetHumidity { get => _targetHumidity; set => SetProperty(ref _targetHumidity, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _currentTemp;
|
||||
public double CurrentTemp { get => _currentTemp; set => SetProperty(ref _currentTemp, value); }
|
||||
private double _currentHumidity;
|
||||
public double CurrentHumidity { get => _currentHumidity; set => SetProperty(ref _currentHumidity, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand PowerOn { get; } public ICommand PowerOff { get; }
|
||||
public ICommand SetRemote { get; } public ICommand SetLocal { get; }
|
||||
public ICommand SetTemp { get; } public ICommand SetHumidity { get; }
|
||||
public ICommand ReadAll { get; }
|
||||
#endregion
|
||||
|
||||
public RLT1000ViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
PowerOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.远程模式开机(Ct()); Log("环境箱已开机"); }));
|
||||
PowerOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.远程模式关机(Ct()); Log("环境箱已关机"); }));
|
||||
SetRemote = new DelegateCommand(async () => await Exec(async () => { await _dev!.切换为远程模式(Ct()); Log("远程控制"); }));
|
||||
SetLocal = new DelegateCommand(async () => await Exec(async () => { await _dev!.切换为本地模式(Ct()); Log("本地控制"); }));
|
||||
SetTemp = new DelegateCommand(async () => await Exec(async () => { await _dev!.定值温度设定(TargetTemp, Ct()); Log($"温度={TargetTemp}℃"); }));
|
||||
SetHumidity = new DelegateCommand(async () => await Exec(async () => { await _dev!.定值湿度设定(TargetHumidity, Ct()); Log($"湿度={TargetHumidity}%RH"); }));
|
||||
ReadAll = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
CurrentTemp = await _dev!.读取当前温度(Ct());
|
||||
CurrentHumidity = await _dev!.读取当前湿度(Ct());
|
||||
Log($"环境→T:{CurrentTemp}℃ H:{CurrentHumidity}%RH");
|
||||
}));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#region 初始化 / Navigation
|
||||
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
RLT1000? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is RLT1000 e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is RLT1000 it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "RLT1000 (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 RLT1000 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
? line + "\n" + ResponseLog[..3000]
|
||||
: line + "\n" + ResponseLog;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using DeviceCommand.Devices;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// S7200 高压直流源载一体机 控制面板 ViewModel
|
||||
/// </summary>
|
||||
public class S7200ViewModel : NavigateViewModelBase, IDisposable
|
||||
{
|
||||
private readonly DeviceManager _dm;
|
||||
private S7200? _dev;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
private string _deviceName = "S7200";
|
||||
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
|
||||
private bool _isConnected;
|
||||
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
|
||||
private bool _isBusy;
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
|
||||
#region 输入参数
|
||||
private double _voltage; public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
|
||||
private double _current; public double Current { get => _current; set => SetProperty(ref _current, value); }
|
||||
private double _cvPosI = 10; public double CvPosI { get => _cvPosI; set => SetProperty(ref _cvPosI, value); }
|
||||
private double _cvNegI = -10; public double CvNegI { get => _cvNegI; set => SetProperty(ref _cvNegI, value); }
|
||||
private double _ovpValue; public double OvpValue { get => _ovpValue; set => SetProperty(ref _ovpValue, value); }
|
||||
private double _ocpValue; public double OcpValue { get => _ocpValue; set => SetProperty(ref _ocpValue, value); }
|
||||
#endregion
|
||||
|
||||
#region 测量结果
|
||||
private double _measuredVoltage;
|
||||
public double MeasuredVoltage { get => _measuredVoltage; set => SetProperty(ref _measuredVoltage, value); }
|
||||
private double _measuredCurrent;
|
||||
public double MeasuredCurrent { get => _measuredCurrent; set => SetProperty(ref _measuredCurrent, value); }
|
||||
private double _measuredPower;
|
||||
public double MeasuredPower { get => _measuredPower; set => SetProperty(ref _measuredPower, value); }
|
||||
private string _responseLog = "";
|
||||
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand QueryIdn { get; } public ICommand OutOn { get; } public ICommand OutOff { get; }
|
||||
public ICommand SetRemote { get; } public ICommand ClearProt { get; }
|
||||
public ICommand SetV { get; } public ICommand SetCC { get; } public ICommand SetCVPos { get; } public ICommand SetCVNeg { get; }
|
||||
public ICommand SetOVP { get; } public ICommand SetOCP { get; }
|
||||
public ICommand QMeas { get; }
|
||||
#endregion
|
||||
|
||||
public S7200ViewModel(IContainerProvider cp) : base(cp)
|
||||
{
|
||||
_dm = cp.Resolve<DeviceManager>();
|
||||
QueryIdn = new DelegateCommand(async () => await Exec(async () => Log("IDN:" + await _dev!.查询设备信息(Ct()))));
|
||||
OutOn = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置通道开关(true, Ct()); Log("输出已开启"); }));
|
||||
OutOff = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置通道开关(false, Ct()); Log("输出已关闭"); }));
|
||||
SetRemote = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置为远程模式(Ct()); Log("远程控制"); }));
|
||||
ClearProt = new DelegateCommand(async () => await Exec(async () => { await _dev!.清除保护状态(Ct()); Log("保护已清除"); }));
|
||||
SetV = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置电压(Voltage, Ct()); Log($"电压={Voltage}V"); }));
|
||||
SetCC = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置CC模式电流(Current, Ct()); Log($"CC电流={Current}A"); }));
|
||||
SetCVPos = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置CV正向电流(CvPosI, Ct()); Log($"CV正向电流={CvPosI}A"); }));
|
||||
SetCVNeg = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置CV反向电流(CvNegI, Ct()); Log($"CV反向电流={CvNegI}A"); }));
|
||||
SetOVP = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置电压保护OVP电压(OvpValue, Ct()); Log($"OVP={OvpValue}V"); }));
|
||||
SetOCP = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置电流保护OCP电流(OcpValue, Ct()); Log($"OCP={OcpValue}A"); }));
|
||||
QMeas = new DelegateCommand(async () => await Exec(async () =>
|
||||
{
|
||||
MeasuredVoltage = await _dev!.查询实时电压(Ct());
|
||||
MeasuredCurrent = await _dev!.查询实时电流(Ct());
|
||||
MeasuredPower = await _dev!.查询功率(Ct());
|
||||
Log($"测量→V:{MeasuredVoltage} I:{MeasuredCurrent} P:{MeasuredPower}");
|
||||
}));
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#region 初始化 / Navigation
|
||||
|
||||
public void Initialize(string? deviceName = null)
|
||||
{
|
||||
S7200? found = null; string? fn = null;
|
||||
if (deviceName != null && _dm.DeviceMap.TryGetValue(deviceName, out var d) && d is S7200 e)
|
||||
{ found = e; fn = deviceName; }
|
||||
else
|
||||
{
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
if (kv.Value is S7200 it) { found = it; fn = kv.Key; break; }
|
||||
}
|
||||
_dev = found;
|
||||
DeviceName = fn ?? "S7200 (未找到)";
|
||||
IsConnected = _dev?.IsConnected ?? false;
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未在 DeviceManager 中找到 S7200 设备");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext context)
|
||||
{
|
||||
var pName = context.Parameters.GetValue<string?>("DeviceName");
|
||||
Initialize(pName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
private async Task Exec(Func<Task> action)
|
||||
{
|
||||
if (_dev == null) { Log("错误:未关联到设备实例,请检查设备配置。"); return; }
|
||||
if (IsBusy) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsConnected = _dev.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("命令超时或已取消。"); }
|
||||
catch (Exception ex) { Log($"错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
|
||||
ResponseLog = ResponseLog.Length > 4000
|
||||
? line + "\n" + ResponseLog[..3000]
|
||||
: line + "\n" + ResponseLog;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.ANEVH80View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="LightningBolt" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="ANEVH 高压源载一体机"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<GroupBox Header="输出控制" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="开启" Command="{Binding OutOn}"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
|
||||
<Button Content="关闭" Command="{Binding OutOff}"
|
||||
Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="复位" Command="{Binding Reset}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="源模式参数" Margin="0,8,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetV}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电流 (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Current, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetI}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="功率 (W)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Power, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetP}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="负载(SINK)参数" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="负载电流 (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding SinkCurrent, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetSinkI}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="负载功率 (W)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding SinkPower, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetSinkP}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="实时测量" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
|
||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电流" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
|
||||
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="功率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredPower, Mode=OneWay}"/>
|
||||
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding QMeas}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// ANEVH80View.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ANEVH80View : UserControl
|
||||
{
|
||||
public ANEVH80View()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.Chroma61800View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="Flash" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Chroma61800 交流源一拖三"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<GroupBox Header="输出控制" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="开启" Command="{Binding OutOn}"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
|
||||
<Button Content="关闭" Command="{Binding OutOff}"
|
||||
Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="复位" Command="{Binding Reset}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="相模式" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="三相" Command="{Binding SetThreePhase}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="单相" Command="{Binding SetSinglePhase}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="参数设置" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetV}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="频率 (Hz)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Frequency, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetF}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="波形" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Waveform, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetWave}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="实时测量" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="交流电压" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
|
||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="交流电流" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
|
||||
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="频率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredFrequency, Mode=OneWay}"/>
|
||||
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="功率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredPower, Mode=OneWay}"/>
|
||||
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding QMeas}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Chroma61800View.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class Chroma61800View : UserControl
|
||||
{
|
||||
public Chroma61800View()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,162 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.DG1000ZView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
<UserControl x:Class="DeviceEditModule.Views.DG1000ZView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="RadioTower" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="DG1000Z 信号发生器一拖三"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<GroupBox Header="通道输出" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="开启输出" Command="{Binding OutOn}"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
|
||||
<Button Content="关闭输出" Command="{Binding OutOff}"
|
||||
Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="重置" Command="{Binding Reset}" Style="{StaticResource WarnBtn}"/>
|
||||
<Button Content="查询错误" Command="{Binding QueryError}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="波形参数" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="通道号" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Channel, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetFreq}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="频率 (Hz)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Frequency, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetFreq}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="幅度 (Vpp)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Amplitude, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetAmp}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="偏移 (Vdc)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding OffsetVoltage, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetOffset}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="占空比 (%)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding DutyCycle, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetDuty}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="测量查询" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="测量频率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredFrequency, Mode=OneWay}"/>
|
||||
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="测量幅度" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredAmplitude, Mode=OneWay}"/>
|
||||
<TextBlock Text="Vpp" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding QueryCounter}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
|
||||
@@ -1,15 +1,156 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.IT6720View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
<UserControl x:Class="DeviceEditModule.Views.IT6720View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="Flash" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="IT6720 低压电源一拖三"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<GroupBox Header="输出控制" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="开启" Command="{Binding OutOn}"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
|
||||
<Button Content="关闭" Command="{Binding OutOff}"
|
||||
Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="远程控制" Command="{Binding SetRemote}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="本地控制" Command="{Binding SetLocal}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="参数设置" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetV}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电流 (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Current, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetI}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压上限 (V)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding VoltageLimit, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetVLim}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="实时测量" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="实际电压" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
|
||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="实际电流" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
|
||||
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="输出模式" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding OutputMode, Mode=OneWay}"/>
|
||||
<TextBlock Text="" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding QMeas}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.MCc30WView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="Water" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="MCc30W 水冷机一拖三"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="回路控制" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="启动回路" Command="{Binding LoopOn}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="停止回路" Command="{Binding LoopOff}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="保护" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="报警复位" Command="{Binding AlarmReset}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="报警消音" Command="{Binding AlarmSilence}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="回路设定" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="回路号" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Loop, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetTemp}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="温度 (℃)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding TempSet, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetTemp}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="流量 (L/min)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding FlowSet, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetFlow}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="压力 (kPa)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding PressSet, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetPress}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="实时状态" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="出液温度" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding CurrentTemp, Mode=OneWay}"/>
|
||||
<TextBlock Text="℃" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="出液流量" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding CurrentFlow, Mode=OneWay}"/>
|
||||
<TextBlock Text="L/min" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="出液压力" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding CurrentPress, Mode=OneWay}"/>
|
||||
<TextBlock Text="kPa" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="报警信息" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding AlarmInfo, Mode=OneWay}"/>
|
||||
<TextBlock Text="" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding ReadAll}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// MCc30WView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class MCc30WView : UserControl
|
||||
{
|
||||
public MCc30WView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,155 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.PW8001View"
|
||||
<UserControl x:Class="DeviceEditModule.Views.PW8001View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="ChartBar" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="PW8001 功率分析仪"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="复位" Command="{Binding Reset}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="清除状态" Command="{Binding Clear}" Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="测试模式" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="WIDE" Command="{Binding ModeWIDE}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="IEC" Command="{Binding ModeIEC}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="通道设置" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="通道号" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Channel, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding QV}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="测量查询" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
|
||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电流" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
|
||||
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="功率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredPower, Mode=OneWay}"/>
|
||||
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="三相总功率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding TotalPower, Mode=OneWay}"/>
|
||||
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压THD" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding VoltageTHD, Mode=OneWay}"/>
|
||||
<TextBlock Text="%" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电流THD" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding CurrentTHD, Mode=OneWay}"/>
|
||||
<TextBlock Text="%" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding QV}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.RLT1000View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="Thermostat" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="RLT1000 环境箱"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<GroupBox Header="电源控制" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="开机" Command="{Binding PowerOn}"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
|
||||
<Button Content="关机" Command="{Binding PowerOff}"
|
||||
Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="远程控制" Command="{Binding SetRemote}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="本地控制" Command="{Binding SetLocal}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="定值设定" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="目标温度 (℃)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding TargetTemp, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetTemp}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="目标湿度 (%RH)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding TargetHumidity, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetHumidity}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="实时监测" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="当前温度" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding CurrentTemp, Mode=OneWay}"/>
|
||||
<TextBlock Text="℃" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="当前湿度" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding CurrentHumidity, Mode=OneWay}"/>
|
||||
<TextBlock Text="%RH" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding ReadAll}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// RLT1000View.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class RLT1000View : UserControl
|
||||
{
|
||||
public RLT1000View()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<UserControl x:Class="DeviceEditModule.Views.S7200View"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
mc:Ignorable="d"
|
||||
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||
d:DesignHeight="760" d:DesignWidth="860">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="12">
|
||||
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<materialDesign:PackIcon Kind="Flash" Width="22" Height="22"
|
||||
Foreground="#1565C0" Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="S7200 高压直流源载一体机"
|
||||
FontSize="15" FontWeight="Bold"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
|
||||
FontSize="13" Foreground="#757575"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="12">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Text" Value="未连接"/>
|
||||
<Setter Property="Foreground" Value="#F44336"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Text" Value="已连接"/>
|
||||
<Setter Property="Foreground" Value="#4CAF50"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
|
||||
Margin="12,0,0,0"
|
||||
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</materialDesign:Card>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,4,0">
|
||||
<GroupBox Header="输出控制" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="开启" Command="{Binding OutOn}"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
|
||||
<Button Content="关闭" Command="{Binding OutOff}"
|
||||
Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="系统" Style="{StaticResource ParamLabel}"/>
|
||||
<Button Content="远程控制" Command="{Binding SetRemote}" Style="{StaticResource CmdBtn}"/>
|
||||
<Button Content="清除保护" Command="{Binding ClearProt}" Style="{StaticResource WarnBtn}"/>
|
||||
</StackPanel>
|
||||
<GroupBox Header="源模式参数" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetV}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="CC电流 (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding Current, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetCC}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="CV正向电流 (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding CvPosI, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetCVPos}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="CV反向电流 (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding CvNegI, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetCVNeg}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="保护设置" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="OVP (V)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding OvpValue, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetOVP}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="OCP (A)" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource NumInput}"
|
||||
Text="{Binding OcpValue, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Content="设置" Command="{Binding SetOCP}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="4,0,0,0">
|
||||
<GroupBox Header="实时测量" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电压" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
|
||||
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="电流" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
|
||||
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="功率" Style="{StaticResource ParamLabel}"/>
|
||||
<TextBox Style="{StaticResource MeasureBox}"
|
||||
Text="{Binding MeasuredPower, Mode=OneWay}"/>
|
||||
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
|
||||
</StackPanel>
|
||||
<Button Content="刷新全部测量" Command="{Binding QMeas}" Style="{StaticResource CmdBtn}" HorizontalAlignment="Left" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="设备信息" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,8">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryIdn}" Style="{StaticResource CmdBtn}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="响应日志" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
|
||||
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
|
||||
IsReadOnly="True" TextWrapping="Wrap"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Background="#FAFAFA" BorderThickness="0"
|
||||
VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace DeviceEditModule.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// S7200View.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class S7200View : UserControl
|
||||
{
|
||||
public S7200View()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,19 +82,23 @@ namespace ExportModule.ViewModels
|
||||
public ICommand LoadedCommand { get; }
|
||||
public ICommand QueryCommand { get; }
|
||||
public ICommand ExportCommand { get; }
|
||||
public ICommand ExportReportCommand { get; }
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
private readonly ITestReportService _testReportService;
|
||||
private readonly ITestCheckRecordService _testCheckRecordService;
|
||||
#endregion
|
||||
|
||||
public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
_testReportService = containerProvider.Resolve<ITestReportService>();
|
||||
_testCheckRecordService = containerProvider.Resolve<ITestCheckRecordService>();
|
||||
|
||||
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
||||
QueryCommand = new AsyncDelegateCommand(OnQuery);
|
||||
ExportCommand = new AsyncDelegateCommand(OnExport);
|
||||
ExportReportCommand = new AsyncDelegateCommand(OnExportReportCommand);
|
||||
}
|
||||
|
||||
#region 命令处理
|
||||
@@ -184,6 +188,40 @@ namespace ExportModule.ViewModels
|
||||
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出测试报告:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细
|
||||
/// </summary>
|
||||
private async Task OnExportReportCommand()
|
||||
{
|
||||
if (SelectedTestReport == null)
|
||||
{
|
||||
ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { });
|
||||
return;
|
||||
}
|
||||
|
||||
StatusMessage = "正在查询测试项判断记录...";
|
||||
var checkResult = await _testCheckRecordService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId);
|
||||
if (!checkResult.IsSuccess || checkResult.Data == null || checkResult.Data.Count == 0)
|
||||
{
|
||||
ShowErrorMessageBox("未找到该测试记录的测试项判断数据(需将子程序标记为测试项后运行)。", () => { });
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*",
|
||||
DefaultExt = ".xlsx",
|
||||
FileName = $"测试报告_{SelectedTestReport.Scope}_{SelectedTestReport.StartTime:yyyyMMdd_HHmmss}.xlsx"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true) return;
|
||||
|
||||
var records = checkResult.Data;
|
||||
StatusMessage = $"正在导出 {records.Count} 条测试项判断记录...";
|
||||
await Task.Run(() => ExportReportToExcel(dialog.FileName, records, SelectedTestReport));
|
||||
ShowInfoMessageBox($"导出完成,已保存至:{dialog.FileName}", () => { });
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助方法
|
||||
@@ -245,6 +283,134 @@ namespace ExportModule.ViewModels
|
||||
workbook.SaveAs(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试报告导出:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细(含重复判断、判断时间、数据值)
|
||||
/// </summary>
|
||||
private static void ExportReportToExcel(string filePath, List<TestCheckRecordEntity> records, TestReportModel report)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var ws = workbook.Worksheets.Add("测试报告");
|
||||
const int colCount = 6;
|
||||
|
||||
var passColor = XLColor.FromArgb(0xB2, 0xFF, 0xB2); // 通过 - 浅绿(与现有导出配色一致)
|
||||
var ngColor = XLColor.FromArgb(0xFF, 0xB2, 0xB2); // 失败 - 浅红
|
||||
var headerColor = XLColor.FromArgb(0xEC, 0xEF, 0xF4);
|
||||
|
||||
int row = 1;
|
||||
|
||||
// ===== 标题与信息行 =====
|
||||
ws.Range(row, 1, row, colCount).Merge();
|
||||
ws.Cell(row, 1).Value = "测 试 报 告";
|
||||
ws.Range(row, 1, row, colCount).Style.Font.Bold = true;
|
||||
ws.Range(row, 1, row, colCount).Style.Font.FontSize = 16;
|
||||
ws.Range(row, 1, row, colCount).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||
row++;
|
||||
ws.Cell(row, 1).Value = $"台架:{report.Scope ?? ""}";
|
||||
ws.Cell(row, 3).Value = $"测试文件:{report.FileName ?? ""}";
|
||||
row++;
|
||||
ws.Cell(row, 1).Value = $"开始时间:{report.StartTime:yyyy-MM-dd HH:mm:ss}";
|
||||
ws.Cell(row, 3).Value = $"结束时间:{report.EndTime:yyyy-MM-dd HH:mm:ss}";
|
||||
row += 2;
|
||||
|
||||
// 按测试项分组(保持首次出现顺序)
|
||||
var groups = records.GroupBy(r => r.TestItemName).ToList();
|
||||
|
||||
// ===== 上层:测试项汇总 =====
|
||||
ws.Cell(row, 1).Value = "【测试项汇总】";
|
||||
ws.Cell(row, 1).Style.Font.Bold = true;
|
||||
row++;
|
||||
|
||||
string[] summaryHeaders = { "序号", "测试项", "判定结果", "执行次数", "判定次数", "NG次数" };
|
||||
for (int c = 0; c < summaryHeaders.Length; c++)
|
||||
ws.Cell(row, c + 1).Value = summaryHeaders[c];
|
||||
var headerRange = ws.Range(row, 1, row, colCount);
|
||||
headerRange.Style.Font.Bold = true;
|
||||
headerRange.Style.Fill.BackgroundColor = headerColor;
|
||||
headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||
headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||
row++;
|
||||
|
||||
int seq = 1;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var summaries = group.Where(x => x.IsSummary).ToList();
|
||||
var details = group.Where(x => !x.IsSummary).ToList();
|
||||
bool overallPass = summaries.All(s => s.Pass) && details.All(d => d.Pass);
|
||||
|
||||
ws.Cell(row, 1).Value = seq++;
|
||||
ws.Cell(row, 2).Value = group.Key;
|
||||
ws.Cell(row, 3).Value = overallPass ? "PASS" : "NG";
|
||||
ws.Cell(row, 4).Value = summaries.Count;
|
||||
ws.Cell(row, 5).Value = details.Count;
|
||||
ws.Cell(row, 6).Value = details.Count(d => !d.Pass);
|
||||
|
||||
var dataRange = ws.Range(row, 1, row, colCount);
|
||||
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||
ws.Cell(row, 3).Style.Font.Bold = true;
|
||||
ws.Cell(row, 3).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor;
|
||||
row++;
|
||||
}
|
||||
row++;
|
||||
|
||||
// ===== 下层:判断明细 =====
|
||||
ws.Cell(row, 1).Value = "【判断明细】";
|
||||
ws.Cell(row, 1).Style.Font.Bold = true;
|
||||
row++;
|
||||
|
||||
string[] detailHeaders = { "序号", "判定时间", "步骤名称", "OKExpression", "判定结果", "数据值" };
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var details = group.Where(x => !x.IsSummary).ToList();
|
||||
bool overallPass = group.Where(x => x.IsSummary).All(s => s.Pass) && details.All(d => d.Pass);
|
||||
|
||||
// 测试项块标题行(合并单元格)
|
||||
ws.Range(row, 1, row, colCount).Merge();
|
||||
ws.Cell(row, 1).Value = $"■ 测试项:{group.Key} 总体结果:{(overallPass ? "PASS" : "NG")}";
|
||||
ws.Cell(row, 1).Style.Font.Bold = true;
|
||||
ws.Range(row, 1, row, colCount).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor;
|
||||
row++;
|
||||
|
||||
for (int c = 0; c < detailHeaders.Length; c++)
|
||||
ws.Cell(row, c + 1).Value = detailHeaders[c];
|
||||
var detailHeaderRange = ws.Range(row, 1, row, colCount);
|
||||
detailHeaderRange.Style.Font.Bold = true;
|
||||
detailHeaderRange.Style.Fill.BackgroundColor = headerColor;
|
||||
detailHeaderRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||
detailHeaderRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||
row++;
|
||||
|
||||
if (details.Count == 0)
|
||||
{
|
||||
ws.Range(row, 1, row, colCount).Merge();
|
||||
ws.Cell(row, 1).Value = "(无 OKExpression 判断,结果由步骤执行成败决定)";
|
||||
row++;
|
||||
}
|
||||
|
||||
int detailSeq = 1;
|
||||
foreach (var d in details)
|
||||
{
|
||||
ws.Cell(row, 1).Value = detailSeq++;
|
||||
ws.Cell(row, 2).Value = d.CreateTime.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
ws.Cell(row, 3).Value = d.StepName ?? "";
|
||||
ws.Cell(row, 4).Value = d.OKExpression ?? "";
|
||||
ws.Cell(row, 5).Value = d.Pass ? "PASS" : "NG";
|
||||
ws.Cell(row, 6).Value = d.Values ?? "";
|
||||
|
||||
var detailRange = ws.Range(row, 1, row, colCount);
|
||||
detailRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||
detailRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||
ws.Cell(row, 5).Style.Font.Bold = true;
|
||||
ws.Cell(row, 5).Style.Fill.BackgroundColor = d.Pass ? passColor : ngColor;
|
||||
row++;
|
||||
}
|
||||
row++;
|
||||
}
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
workbook.SaveAs(filePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 开始日期 -->
|
||||
@@ -116,12 +116,19 @@
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="8"
|
||||
Content="导出测试报告"
|
||||
Command="{Binding ExportReportCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Padding="16,6"
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="9"
|
||||
Content="加载全部"
|
||||
Command="{Binding LoadedCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Padding="16,6"
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"/>
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -217,10 +217,10 @@ namespace MainModule.ViewModels
|
||||
(LogAreaVM as IDisposable)?.Dispose();
|
||||
(ParametersManagerVM as IDisposable)?.Dispose();
|
||||
|
||||
_globalInfo.ContextDic?.Remove(TestStatus);
|
||||
_globalInfo.StepRunningDic?.Remove(TestStatus);
|
||||
_globalInfo.ConfigDic?.Remove(TestStatus);
|
||||
_globalInfo.ScopeDic?.Remove(TestStatus);
|
||||
_globalInfo.ContextDic?.TryRemove(TestStatus, out _);
|
||||
_globalInfo.StepRunningDic?.TryRemove(TestStatus, out _);
|
||||
_globalInfo.ConfigDic?.TryRemove(TestStatus, out _);
|
||||
_globalInfo.ScopeDic?.TryRemove(TestStatus, out _);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -234,11 +234,23 @@ namespace MainModule.ViewModels
|
||||
#region 命令处理与事件
|
||||
private async Task OnLoad()
|
||||
{
|
||||
if (!IsInitialized)
|
||||
// 设备初始化期间仅对当前台架显示灰度遮罩层,初始化完成后关闭(finally 保证异常时也能关闭)
|
||||
_eventAggregator.GetEvent<ScopeOverlayEvent>().Publish(new ScopeOverlayArgs { Scope = TestStatus, Show = true });
|
||||
// 让遮罩层先完成渲染,再进入同步阻塞的设备初始化(泵送 Dispatcher 至 Render 优先级)
|
||||
System.Windows.Application.Current?.Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Render);
|
||||
try
|
||||
{
|
||||
await _deviceManager.ConnectAllDevices();
|
||||
IsInitialized = true;
|
||||
if (!IsInitialized)
|
||||
{
|
||||
await _deviceManager.ConnectAllDevices();
|
||||
IsInitialized = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_eventAggregator.GetEvent<ScopeOverlayEvent>().Publish(new ScopeOverlayArgs { Scope = TestStatus, Show = false });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnRefresh()
|
||||
@@ -263,10 +275,10 @@ namespace MainModule.ViewModels
|
||||
if (navigationContext.Parameters.ContainsKey("Name"))
|
||||
{
|
||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||
_globalInfo.ContextDic.Add(TestStatus, _scopedContext);
|
||||
_globalInfo.StepRunningDic.Add(TestStatus, _stepRunning);
|
||||
_globalInfo.ScopeDic.Add(TestStatus, _scope);
|
||||
_globalInfo.ConfigDic.Add(TestStatus, _systemConfig);
|
||||
_globalInfo.ContextDic.TryAdd(TestStatus, _scopedContext);
|
||||
_globalInfo.StepRunningDic.TryAdd(TestStatus, _stepRunning);
|
||||
_globalInfo.ScopeDic.TryAdd(TestStatus, _scope);
|
||||
_globalInfo.ConfigDic.TryAdd(TestStatus, _systemConfig);
|
||||
if(_systemConfig.DefaultProgramFilePath != null&&File.Exists(_systemConfig.DefaultProgramFilePath))
|
||||
{
|
||||
var filePath = _systemConfig.DefaultProgramFilePath;
|
||||
|
||||
@@ -15,41 +15,41 @@ namespace MainModule.ViewModels
|
||||
private bool IsInitialized = false;
|
||||
private string _expandedCellName = string.Empty;
|
||||
#endregion
|
||||
|
||||
|
||||
#region 属性
|
||||
public bool KeepAlive => true; // 保持存活
|
||||
|
||||
|
||||
public string ExpandedCellName
|
||||
{
|
||||
get => _expandedCellName;
|
||||
set => SetProperty(ref _expandedCellName, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
#region 命令
|
||||
public ICommand LoadedCommand { get; set; }
|
||||
#endregion
|
||||
|
||||
|
||||
public MainViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
LoadedCommand = new DelegateCommand(OnLoaded);
|
||||
_eventAggregator.GetEvent<ExpandViewEvent>().Subscribe(OnCellExpandRequested);
|
||||
}
|
||||
|
||||
|
||||
#region 命令处理与事件
|
||||
private void OnLoaded()
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
for (int i = 1; i <= 9; i++)
|
||||
for (int i = 1; i <= 3; i++)
|
||||
{
|
||||
var parameters = new NavigationParameters { { "Name", $"TestCell{i}" } };
|
||||
_regionManager.RequestNavigate($"TestCell{i}", "ProtocolStartView", parameters);
|
||||
}
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
|
||||
// 不再物理搬迁视图,只需修改一个字符串属性 ExpandedCellName,
|
||||
// XAML 里每个单元的 Style.Triggers 会根据该值处理隐藏 / 跨越 3x3。
|
||||
// XAML 里每个单元的 Style.Triggers 会根据该值处理隐藏 / 跨越整行。
|
||||
private void OnCellExpandRequested(string cellName)
|
||||
{
|
||||
ExpandedCellName = cellName ?? string.Empty;
|
||||
|
||||
@@ -128,6 +128,25 @@
|
||||
</vs:ParametersManager.Style>
|
||||
</vs:ParametersManager>
|
||||
|
||||
<Border x:Name="Overlay"
|
||||
Background="#40000000"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.ColumnSpan="4">
|
||||
<StackPanel Width="150"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 0 100">
|
||||
<ProgressBar Width="80"
|
||||
Height="80"
|
||||
Margin="20"
|
||||
IsIndeterminate="True"
|
||||
Style="{StaticResource MaterialDesignCircularProgressBar}" />
|
||||
<TextBlock FontSize="30"
|
||||
Text="加载中......"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Prism.Events;
|
||||
using MainModule.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -12,6 +14,7 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using UIShare.PubEvent;
|
||||
|
||||
namespace MainModule.Views
|
||||
{
|
||||
@@ -20,9 +23,23 @@ namespace MainModule.Views
|
||||
/// </summary>
|
||||
public partial class AutomatedTestingView : UserControl
|
||||
{
|
||||
public AutomatedTestingView()
|
||||
public AutomatedTestingView(IEventAggregator eventAggregator)
|
||||
{
|
||||
InitializeComponent();
|
||||
eventAggregator.GetEvent<OverlayEvent>().Subscribe(ShowOverlay);
|
||||
// 台架级加载遮罩:仅台架名称匹配时响应,避免其他台架/主窗口同时变灰
|
||||
eventAggregator.GetEvent<ScopeOverlayEvent>().Subscribe(ShowScopeOverlay);
|
||||
}
|
||||
private void ShowOverlay(bool arg)
|
||||
{
|
||||
Overlay.Visibility = arg ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ShowScopeOverlay(ScopeOverlayArgs args)
|
||||
{
|
||||
// 只处理属于当前台架的遮罩事件(TestStatus 在 OnNavigatedTo 时赋值)
|
||||
if (DataContext is not AutomatedTestingViewModel vm || vm.TestStatus != args.Scope) return;
|
||||
Overlay.Visibility = args.Show ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,75 +24,9 @@
|
||||
|
||||
<!-- 顶部的选项卡,切换 3 个台架 -->
|
||||
<TabControl Margin="5">
|
||||
|
||||
<TabControl.Resources>
|
||||
<!-- 1. 去除 TabControl 原生的丑陋边框和灰色背景 -->
|
||||
<Style TargetType="TabControl">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="0,5,0,0"/>
|
||||
</Style>
|
||||
|
||||
<!-- 2. 美化 TabItem 标签头 -->
|
||||
<Style TargetType="TabItem">
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="#8C8C8C"/>
|
||||
<!-- 默认灰字 -->
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Padding" Value="25,12"/>
|
||||
<!-- 撑大点击区域 -->
|
||||
<Setter Property="Margin" Value="0,0,4,0"/>
|
||||
<!-- 标签之间的间距 -->
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
|
||||
<!-- 核心魔法:重写外观结构 -->
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TabItem">
|
||||
<!-- 用一个带底部边框的 Border 包装文字 -->
|
||||
<Border Name="Border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0,0,0,3"
|
||||
CornerRadius="4,4,0,0">
|
||||
<ContentPresenter x:Name="ContentSite"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
ContentSource="Header"
|
||||
Margin="{TemplateBinding Padding}"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- 交互状态1:鼠标悬浮 -->
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#E6E6E6"/>
|
||||
<Setter Property="Foreground" Value="#595959"/>
|
||||
</Trigger>
|
||||
|
||||
<!-- 交互状态2:被选中 -->
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="White"/>
|
||||
<!-- 选中时的底部指示条颜色(蓝色) -->
|
||||
<Setter TargetName="Border" Property="BorderBrush" Value="#007ACC"/>
|
||||
<!-- 选中时的字体颜色(蓝色) -->
|
||||
<Setter Property="Foreground" Value="#007ACC"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
|
||||
<Style.Triggers>
|
||||
<!-- !!!这是你原有的全屏隐藏逻辑!完美保留!!! -->
|
||||
<DataTrigger Binding="{Binding ExpandedCellName, Converter={StaticResource StringToVisibilityConverter}}" Value="Visible">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TabControl.Resources>
|
||||
|
||||
<!-- 标签页 1 -->
|
||||
<TabItem Header="台架 1 (Bench 1)">
|
||||
<!-- 标签页 1:Inverse 参数 = 展开全屏(ExpandedCellName 非空)时隐藏标签页,不动主题样式 -->
|
||||
<TabItem Header="台架 1 (Bench 1)"
|
||||
Visibility="{Binding ExpandedCellName, Converter={StaticResource StringToVisibilityConverter}, ConverterParameter=Inverse}">
|
||||
<!-- 加一个白底包裹,显得更像现代卡片 -->
|
||||
<Border Background="White" CornerRadius="0,4,4,4" Margin="0" BorderThickness="0" Padding="5">
|
||||
<ContentControl prism:RegionManager.RegionName="TestCell1" />
|
||||
@@ -100,14 +34,16 @@
|
||||
</TabItem>
|
||||
|
||||
<!-- 标签页 2 -->
|
||||
<TabItem Header="台架 2 (Bench 2)">
|
||||
<TabItem Header="台架 2 (Bench 2)"
|
||||
Visibility="{Binding ExpandedCellName, Converter={StaticResource StringToVisibilityConverter}, ConverterParameter=Inverse}">
|
||||
<Border Background="White" CornerRadius="0,4,4,4" Margin="0" BorderThickness="0" Padding="5">
|
||||
<ContentControl prism:RegionManager.RegionName="TestCell2" />
|
||||
</Border>
|
||||
</TabItem>
|
||||
|
||||
<!-- 标签页 3 -->
|
||||
<TabItem Header="台架 3 (Bench 3)">
|
||||
<TabItem Header="台架 3 (Bench 3)"
|
||||
Visibility="{Binding ExpandedCellName, Converter={StaticResource StringToVisibilityConverter}, ConverterParameter=Inverse}">
|
||||
<Border Background="White" CornerRadius="0,4,4,4" Margin="0" BorderThickness="0" Padding="5">
|
||||
<ContentControl prism:RegionManager.RegionName="TestCell3" />
|
||||
</Border>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using SqlSugar;
|
||||
using System;
|
||||
|
||||
namespace Model.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试项判断记录:运行过程中测试项(IsTestItem 子程序)范围内每一次 OKExpression 判断。
|
||||
/// 同一次运行的所有记录共享同一个 TestRoundId,导出测试报告时按此 Guid 查询。
|
||||
/// IsSummary=true 的行为该测试项单次执行的汇总(PASS/NG),IsSummary=false 的行为单次判断明细。
|
||||
/// </summary>
|
||||
public class TestCheckRecordEntity : BaseEntity
|
||||
{
|
||||
/// <summary>同一次运行的统一标识(StepRunning.TestRoundID)</summary>
|
||||
[SugarColumn(ColumnName = "TestRoundId", ColumnDescription = "运行轮次标识")]
|
||||
public Guid TestRoundId { get; set; }
|
||||
|
||||
/// <summary>作用域/台架名称</summary>
|
||||
[SugarColumn(ColumnName = "Scope", ColumnDescription = "台架名称")]
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>测试项名称(当前ADP文件路径)</summary>
|
||||
[SugarColumn(ColumnName = "FileName", ColumnDescription = "测试项名称")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>测试项名称(IsTestItem 子程序步骤的名称)</summary>
|
||||
[SugarColumn(ColumnName = "TestItemName", Length = 200)]
|
||||
public string TestItemName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>发生判断的步骤名称</summary>
|
||||
[SugarColumn(ColumnName = "StepName", Length = 200)]
|
||||
public string StepName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>子程序嵌套深度(0=主程序)</summary>
|
||||
[SugarColumn(ColumnName = "Depth")]
|
||||
public int Depth { get; set; }
|
||||
|
||||
/// <summary>判断表达式</summary>
|
||||
[SugarColumn(ColumnName = "OKExpression", Length = 1000, IsNullable = true)]
|
||||
public string? OKExpression { get; set; }
|
||||
|
||||
/// <summary>判断结果(true=PASS / false=NG)</summary>
|
||||
[SugarColumn(ColumnName = "Pass")]
|
||||
public bool Pass { get; set; }
|
||||
|
||||
/// <summary>表达式变量的实际取值(如 "电压=12.5; 电流=3.2; ")</summary>
|
||||
[SugarColumn(ColumnName = "Values", Length = 2000, IsNullable = true)]
|
||||
public string? Values { get; set; }
|
||||
|
||||
/// <summary>是否为汇总行(每个测试项单次执行结束时写入一条)</summary>
|
||||
[SugarColumn(ColumnName = "IsSummary")]
|
||||
public bool IsSummary { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ namespace Model.Models
|
||||
|
||||
public bool IsUsed { get; set; } = true;
|
||||
|
||||
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
|
||||
public bool IsTestItem { get; set; } = false;
|
||||
|
||||
public int Index { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
@@ -197,7 +197,6 @@ namespace MonitorModule.ViewModels.Dialogs
|
||||
/// </summary>
|
||||
private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverCanSignals()
|
||||
{
|
||||
if (_deviceManager?.CANFD == null) yield break;
|
||||
if (_systemConfig?.ConfigurationList == null) yield break;
|
||||
|
||||
var msgDb = DBCParse.MsgDatabase;
|
||||
@@ -331,7 +330,6 @@ namespace MonitorModule.ViewModels.Dialogs
|
||||
private void OnDbcLoaded(DBCLoadedArgs args)
|
||||
{
|
||||
if (args.Scope != _systemConfig?.Title) return;
|
||||
if (_deviceManager?.CANFD == null) return;
|
||||
|
||||
int channel = (int)args.Channel;
|
||||
var msgDb = DBCParse.MsgDatabase;
|
||||
|
||||
@@ -182,6 +182,9 @@ namespace MonitorModule.ViewModels
|
||||
try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { }
|
||||
}
|
||||
|
||||
// 从 CAN 广播器注销当前作用域配置(一拖三场景:工位销毁时清理注册表)
|
||||
if (!string.IsNullOrEmpty(TestStatus))
|
||||
_canSignalBroadcaster?.UnregisterScope(TestStatus);
|
||||
|
||||
// 3. 释放容器作用域
|
||||
_scope?.Dispose();
|
||||
@@ -335,6 +338,8 @@ namespace MonitorModule.ViewModels
|
||||
// 4. 启动广播器(Discover + Start)
|
||||
_broadcaster.Discover();
|
||||
_broadcaster.Start();
|
||||
// 将当前工位的 CAN 配置注册到全局广播器(一拖三场景:每个工位各自的 DBC/ConfigurationList)
|
||||
_canSignalBroadcaster.RegisterScope(_systemConfig.Title, _systemConfig);
|
||||
_canSignalBroadcaster.Discover();
|
||||
_canSignalBroadcaster.Start();
|
||||
|
||||
@@ -535,7 +540,6 @@ namespace MonitorModule.ViewModels
|
||||
private void RefreshConfiguredCanSignals()
|
||||
{
|
||||
if (_systemConfig?.ConfigurationList == null || _systemConfig.ConfigurationList.Count == 0) return;
|
||||
if (_deviceManager?.CANFD == null) return;
|
||||
|
||||
var msgDb = DBCParse.MsgDatabase;
|
||||
foreach (var channel in _systemConfig.ConfigurationList.Select(c => c.Channel).Distinct())
|
||||
@@ -633,7 +637,6 @@ namespace MonitorModule.ViewModels
|
||||
private void OnDbcLoaded(DBCLoadedArgs args)
|
||||
{
|
||||
if (args.Scope != TestStatus) return;
|
||||
if (_deviceManager?.CANFD == null) return;
|
||||
int channel = (int)args.Channel;
|
||||
var msgDb = DBCParse.MsgDatabase;
|
||||
if (channel < 0 || channel >= msgDb.Count) return;
|
||||
|
||||
@@ -128,5 +128,44 @@ namespace ORM
|
||||
throw new Exception("连接数据库失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归档过期数据库文件:将旧的 SQLite 文件重命名为 "SQL_归档_yyyyMMdd_HHmmss.db",
|
||||
/// 后续 <see cref="CreateDatabaseAndCheckConnection"/> 会自动创建新的空数据库。
|
||||
/// <para>
|
||||
/// 必须在 <see cref="InitSqlite"/> 之后、<see cref="CreateDatabaseAndCheckConnection"/> 之前调用,
|
||||
/// 此时连接字符串已就绪但数据库文件尚未被打开。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="retentionDays">数据库文件保留天数,默认 180 天(半年)</param>
|
||||
public static void TryArchiveOldDatabase(int retentionDays = 180)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从连接字符串中提取文件路径(格式:Data Source=xxx;)
|
||||
string dbPath = DbConnectionString
|
||||
.Replace("Data Source=", "", StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd(';');
|
||||
|
||||
if (!File.Exists(dbPath)) return;
|
||||
|
||||
var cutoff = DateTime.Now.AddDays(-retentionDays);
|
||||
var fileInfo = new FileInfo(dbPath);
|
||||
|
||||
if (fileInfo.LastWriteTime < cutoff)
|
||||
{
|
||||
string archiveName = $"SQL_归档_{fileInfo.LastWriteTime:yyyyMMdd_HHmmss}.db";
|
||||
string archivePath = Path.Combine(fileInfo.DirectoryName!, archiveName);
|
||||
|
||||
File.Move(dbPath, archivePath);
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"[数据库归档] {dbPath} → {archivePath}(文件最后修改于 {fileInfo.LastWriteTime:yyyy-MM-dd},已超过 {retentionDays} 天)");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"数据库归档失败(不影响软件正常使用):{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Model;
|
||||
using Model.Entity;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
public class TestCheckRecordService : BaseService<TestCheckRecordEntity>, ITestCheckRecordService
|
||||
{
|
||||
public TestCheckRecordService(SqlSugarRepository<TestCheckRecordEntity> repository) : base(repository)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
|
||||
/// </summary>
|
||||
public async Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await _repository.Entities
|
||||
.Where(x => x.TestRoundId == testRoundId)
|
||||
.OrderBy(x => x.CreateTime)
|
||||
.ToListAsync();
|
||||
|
||||
return Result<List<TestCheckRecordEntity>>.Success(list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<TestCheckRecordEntity>>.Error("根据 TestRoundId 查询测试项判断记录失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Model;
|
||||
using Model.Entity;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Interface
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试项判断记录服务:运行时记录测试项范围内每一次 OKExpression 判断,导出测试报告时查询。
|
||||
/// </summary>
|
||||
public interface ITestCheckRecordService : IBaseService<TestCheckRecordEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
|
||||
/// </summary>
|
||||
Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId);
|
||||
}
|
||||
}
|
||||
+67
-322
@@ -10,276 +10,20 @@ using System.Text.Json;
|
||||
using TSMaster;
|
||||
namespace TSMasterCAN
|
||||
{
|
||||
/// <summary>
|
||||
/// TSMaster CAN/CANFD 总线控制类,基于 TSMaster API 实现总线连接管理、
|
||||
/// DBC 信号读写、报文发送(单次/循环)、日志记录等功能,暴露给命令树供测试步骤调用。
|
||||
/// </summary>
|
||||
[ACPCommand]
|
||||
public class CAN : IDisposable
|
||||
public class CAN
|
||||
{
|
||||
#region 静态成员
|
||||
|
||||
public static event Action? ConnectEvent;
|
||||
public static event Action? DisConnectEvent;
|
||||
public static event Action ConnectEvent;
|
||||
public static event Action DisConnectEvent;
|
||||
public static bool ConnectFlag { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 实时接收到的 CAN 报文缓存(按报文 ID 索引,保留最新帧)。
|
||||
/// 供 CANViewModel.Refresh 等界面轮询读取。
|
||||
/// </summary>
|
||||
public static ConcurrentDictionary<int, TLIBCANFD> RealTimeMessages { get; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 实例字段与属性
|
||||
|
||||
/// <summary>设备类型号(43 = USBCANFD-400U 等,TSMaster 中作为配置参考保留)</summary>
|
||||
public uint DeviceType { get; }
|
||||
/// <summary>设备索引</summary>
|
||||
public uint DeviceIndex { get; }
|
||||
/// <summary>最大通道数</summary>
|
||||
public int MaxChannels { get; }
|
||||
/// <summary>仲裁域波特率</summary>
|
||||
public string ABitBaud { get; }
|
||||
/// <summary>数据域波特率</summary>
|
||||
public string DBitBaud { get; }
|
||||
/// <summary>是否开启终端电阻</summary>
|
||||
public bool EnableTerminalResistance { get; }
|
||||
|
||||
/// <summary>DBC 解析器(提供 MsgDatabase、MaxChannels 等访问)</summary>
|
||||
public DBCParse DBCParser => DBCParse.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// DBC 报文解码事件:当接收到 CAN 帧并通过 DBC 解码后触发。
|
||||
/// 参数:(通道号, 解码后的报文信息)
|
||||
/// </summary>
|
||||
public event Action<uint, DBCMessage>? OnDbcMessageDecoded;
|
||||
|
||||
private bool _disposed;
|
||||
private bool _isOpened;
|
||||
private TCANFDQueueEvent_Win32? _instanceListener;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>默认构造函数(兼容 SystemConfig.CANFD = new() 等场景)</summary>
|
||||
public CAN() : this(43, 0, 4, "500000", "2000000", true) { }
|
||||
|
||||
/// <summary>
|
||||
/// 带配置参数的构造函数(兼容原 ZLG 设备管理流程)。
|
||||
/// </summary>
|
||||
/// <param name="deviceType">设备类型号</param>
|
||||
/// <param name="deviceIndex">设备索引</param>
|
||||
/// <param name="maxChannels">最大通道数</param>
|
||||
/// <param name="aBitBaud">仲裁域波特率</param>
|
||||
/// <param name="dBitBaud">数据域波特率</param>
|
||||
/// <param name="enableTerminalResistance">是否开启终端电阻</param>
|
||||
public CAN(uint deviceType, uint deviceIndex, int maxChannels, string aBitBaud, string dBitBaud, bool enableTerminalResistance)
|
||||
{
|
||||
DeviceType = deviceType;
|
||||
DeviceIndex = deviceIndex;
|
||||
MaxChannels = maxChannels;
|
||||
ABitBaud = aBitBaud;
|
||||
DBitBaud = dBitBaud;
|
||||
EnableTerminalResistance = enableTerminalResistance;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 实例方法 — 设备管理(替代原 ZLG 实例方法)
|
||||
|
||||
/// <summary>
|
||||
/// 打开设备:初始化 TSMaster 库 → 设置通道数 → 配置波特率 → 连接 → 启动 RBS → 注册监听。
|
||||
/// </summary>
|
||||
/// <returns>true 表示成功</returns>
|
||||
public bool 打开设备()
|
||||
{
|
||||
if (_isOpened) return true;
|
||||
if (ConnectFlag) { _isOpened = true; return true; }
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 初始化 TSMaster 库
|
||||
var re = Init("ACP");
|
||||
if (re != 0)
|
||||
{
|
||||
Debug.WriteLine($"TSMaster 初始化失败,错误代码:{re}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 设置通道数
|
||||
TsMasterApi.tsapp_set_can_channel_count(MaxChannels);
|
||||
|
||||
// 3. 配置各通道波特率
|
||||
float arbKbps = float.Parse(ABitBaud) / 1000f;
|
||||
float dataKbps = float.Parse(DBitBaud) / 1000f;
|
||||
for (int ch = 0; ch < MaxChannels; ch++)
|
||||
{
|
||||
TsMasterApi.tsapp_configure_baudrate_canfd(
|
||||
ch, arbKbps, dataKbps,
|
||||
TLIBCANFDControllerType.lfdtISOCAN,
|
||||
TLIBCANFDControllerMode.lfdmNormal,
|
||||
EnableTerminalResistance);
|
||||
}
|
||||
|
||||
// 4. 连接硬件
|
||||
re = Connect();
|
||||
if (re != 0)
|
||||
{
|
||||
Debug.WriteLine($"CAN 连接失败,错误代码:{re}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 注册接收监听(用于 DBC 信号解码广播)
|
||||
_instanceListener = new TCANFDQueueEvent_Win32(OnCanFdReceived);
|
||||
RegisterListener(_instanceListener);
|
||||
|
||||
_isOpened = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"打开设备异常:{ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭 CAN 卡设备:注销监听 → 断开连接。
|
||||
/// </summary>
|
||||
public void 关闭CAN卡设备()
|
||||
{
|
||||
if (!_isOpened && !ConnectFlag) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (_instanceListener != null)
|
||||
{
|
||||
UnRegisterListener(_instanceListener);
|
||||
_instanceListener = null;
|
||||
}
|
||||
DisConnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"关闭CAN卡设备异常:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化并启动指定通道(配置波特率,TSMaster 在 Connect 时已自动启动所有通道)。
|
||||
/// </summary>
|
||||
/// <param name="channel">通道号</param>
|
||||
public void 初始化并启动通道(uint channel)
|
||||
{
|
||||
if (channel >= MaxChannels) return;
|
||||
try
|
||||
{
|
||||
float arbKbps = float.Parse(ABitBaud) / 1000f;
|
||||
float dataKbps = float.Parse(DBitBaud) / 1000f;
|
||||
TsMasterApi.tsapp_configure_baudrate_canfd(
|
||||
(int)channel, arbKbps, dataKbps,
|
||||
TLIBCANFDControllerType.lfdtISOCAN,
|
||||
TLIBCANFDControllerMode.lfdmNormal,
|
||||
EnableTerminalResistance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"初始化通道 {channel} 异常:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载通道 DBC 文件。
|
||||
/// </summary>
|
||||
/// <param name="channel">通道号</param>
|
||||
/// <param name="filePath">DBC 文件路径</param>
|
||||
/// <returns>true 表示加载成功</returns>
|
||||
public bool 加载通道DBC文件(uint channel, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var re = LoadDBC(filePath, [(int)channel], out _);
|
||||
return re == 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"加载通道 {channel} DBC 文件异常:{ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CAN FD 接收回调:接收原始帧 → 查找 DBC → 解码信号 → 触发 OnDbcMessageDecoded 事件。
|
||||
/// </summary>
|
||||
private void OnCanFdReceived(ref int AObj, ref TLIBCANFD AData)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// 始终缓存最新报文,供界面轮询刷新
|
||||
int identifier = AData.FIdentifier;
|
||||
var canfdCopy = AData;
|
||||
RealTimeMessages[identifier] = canfdCopy;
|
||||
|
||||
if (OnDbcMessageDecoded == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
uint channel = AData.FIdxChn;
|
||||
if (channel >= DBCParse.MsgDatabase.Count) return;
|
||||
|
||||
// 在 DBC 数据库中查找匹配的报文
|
||||
var msgList = DBCParse.MsgDatabase[(int)channel];
|
||||
var match = msgList.FirstOrDefault(m => m.msg_id == identifier);
|
||||
if (match == null || match.signal_Name == null || match.signal_Name.Length == 0) return;
|
||||
|
||||
// 解码各信号值
|
||||
var signals = new DBCSignal[match.signal_Name.Length];
|
||||
for (int i = 0; i < match.signal_Name.Length; i++)
|
||||
{
|
||||
double value = 0;
|
||||
TsMasterApi.tsdb_get_signal_value_canfd(ref canfdCopy, match.msg_name, match.signal_Name[i], ref value);
|
||||
signals[i] = new DBCSignal
|
||||
{
|
||||
strName = Encoding.Default.GetBytes(match.signal_Name[i]),
|
||||
nRawvalue = value,
|
||||
nFactor = 1,
|
||||
nOffset = 0
|
||||
};
|
||||
}
|
||||
|
||||
var decoded = new DBCMessage
|
||||
{
|
||||
nID = (uint)identifier,
|
||||
nSignalCount = signals.Length,
|
||||
vSignals = signals
|
||||
};
|
||||
|
||||
OnDbcMessageDecoded?.Invoke(channel, decoded);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"DBC 信号解码异常:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
关闭CAN卡设备();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 初始化TSMasterCAN
|
||||
/// 初始化CAN驱动
|
||||
/// </summary>
|
||||
/// <param name="ProjectName"></param>
|
||||
/// <param name="filePath"></param>
|
||||
@@ -298,7 +42,7 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放TSMasterCAN
|
||||
/// 释放CAN驱动
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public static void Release()
|
||||
@@ -330,9 +74,10 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接
|
||||
/// 连接 TSMaster CAN 总线:建立总线连接并启动 RBS 仿真系统,
|
||||
/// 成功后置位连接标志并触发 <see cref="ConnectEvent"/> 连接事件。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
|
||||
public static int Connect()
|
||||
{
|
||||
|
||||
@@ -359,9 +104,9 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开
|
||||
/// 断开 TSMaster CAN 总线连接,成功后清除连接标志并触发 <see cref="DisConnectEvent"/> 断开事件。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败(可用 <see cref="GetErrorDescription"/> 查询错误说明)</returns>
|
||||
public static int DisConnect()
|
||||
{
|
||||
var re = TsMasterApi.tsapp_disconnect();
|
||||
@@ -391,7 +136,7 @@ namespace TSMasterCAN
|
||||
public static int LoadDBC(string filePath, int[] channel, out uint databaseID)
|
||||
{
|
||||
databaseID = 0;
|
||||
|
||||
if (!Path.Exists(filePath)) return -1;
|
||||
var re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
|
||||
foreach (var item in channel)
|
||||
{
|
||||
@@ -421,19 +166,19 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始记录日志
|
||||
/// 开始记录总线日志,将总线上的报文数据记录到指定文件。
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="filePath">日志文件保存路径(自动转换为绝对路径)</param>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
public static int StartLogging(string filePath)
|
||||
{
|
||||
return TsMasterApi.tsapp_start_logging(Path.GetFullPath(filePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束记录日志
|
||||
/// 结束总线日志记录。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
public static int StopLogging()
|
||||
{
|
||||
return TsMasterApi.tsapp_stop_logging();
|
||||
@@ -451,12 +196,12 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取信号值
|
||||
/// 获取 DBC 中定义信号的当前物理值(从总线接收报文中解析)。
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="AMsgName"></param>
|
||||
/// <param name="ASgnName"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="channel">通道号(0 起始)</param>
|
||||
/// <param name="AMsgName">DBC 中的报文名称</param>
|
||||
/// <param name="ASgnName">DBC 中的信号名称</param>
|
||||
/// <returns>信号物理值,获取失败时返回 <see cref="double.NaN"/></returns>
|
||||
public static double GetSignalValue(byte channel, string AMsgName, string ASgnName)
|
||||
{
|
||||
double value = double.NaN;
|
||||
@@ -497,16 +242,16 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置信号值
|
||||
/// 设置 DBC 中定义信号的值(按物理值编码进报文数据),可选择立即发送或周期发送。
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="AMsgName"></param>
|
||||
/// <param name="ASgnName"></param>
|
||||
/// <param name="AValue"></param>
|
||||
/// <param name="isSend"></param>
|
||||
/// <param name="sendPeriod"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
/// <param name="channel">通道号(0 起始)</param>
|
||||
/// <param name="AMsgName">DBC 中的报文名称</param>
|
||||
/// <param name="ASgnName">DBC 中的信号名称</param>
|
||||
/// <param name="AValue">要设置的信号物理值</param>
|
||||
/// <param name="isSend">是否发送:false 仅更新数据不发送,true 则发送</param>
|
||||
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送</param>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
/// <exception cref="Exception">信号编码写入报文失败时抛出</exception>
|
||||
public static int SetSignalValue(byte channel, string AMsgName, string ASgnName, double AValue, bool isSend = false, float sendPeriod = 0)
|
||||
{
|
||||
var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
|
||||
@@ -529,14 +274,14 @@ namespace TSMasterCAN
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置信号值
|
||||
/// 按报文名称发送 DBC 中定义的报文(使用该报文在 DBC 数据库中的当前数据)。
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="AMsgName"></param>
|
||||
/// <param name="sendPeriod"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="channel">通道号</param>
|
||||
/// <param name="AMsgName">DBC 中的报文名称</param>
|
||||
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送</param>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
public static int SetSignalValue(APP_CHANNEL channel, string AMsgName, float sendPeriod = 0)
|
||||
{
|
||||
var find = DBCParse.MsgDatabase[(byte)channel].First(s => s.msg_name == AMsgName);
|
||||
@@ -562,11 +307,11 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置报文
|
||||
/// 按帧 ID 更新 DBC 数据库中指定报文的数据区(仅更新内存中的数据,不发送)。
|
||||
/// </summary>
|
||||
/// <param name="channel">通道</param>
|
||||
/// <param name="ID">DBC数据库ID</param>
|
||||
/// <param name="bytes">报文数组</param>
|
||||
/// <param name="channel">通道号</param>
|
||||
/// <param name="ID">报文帧 ID(DBC 中定义的 FIdentifier)</param>
|
||||
/// <param name="bytes">要写入的报文数据数组,超出报文数据长度的部分会被截断</param>
|
||||
public static void SetMsg(APP_CHANNEL channel, int ID, byte[] bytes)
|
||||
{
|
||||
var find = DBCParse.MsgDatabase[(byte)channel].FirstOrDefault(s => s.ACANFD.FIdentifier == ID);
|
||||
@@ -577,20 +322,20 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送自定义报文
|
||||
/// 构造并发送自定义 CAN/CANFD 报文,可选择单次发送或周期发送,并可同步更新 DBC 数据库中同 ID 报文的数据。
|
||||
/// </summary>
|
||||
/// <param name="AIdxChn"></param>
|
||||
/// <param name="AID"></param>
|
||||
/// <param name="AIsTx"></param>
|
||||
/// <param name="AIsExt"></param>
|
||||
/// <param name="AIsRemote"></param>
|
||||
/// <param name="ADLC"></param>
|
||||
/// <param name="ADataArray"></param>
|
||||
/// <param name="AIsFD"></param>
|
||||
/// <param name="AIsBRS"></param>
|
||||
/// <param name="isUpdateDBCDatabase"></param>
|
||||
/// <param name="sendPeriod"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="AIdxChn">通道号</param>
|
||||
/// <param name="AID">报文帧 ID</param>
|
||||
/// <param name="AIsTx">是否为发送帧</param>
|
||||
/// <param name="AIsExt">是否为扩展帧(29 位 ID)</param>
|
||||
/// <param name="AIsRemote">是否为远程帧</param>
|
||||
/// <param name="ADLC">数据长度代码(数据字节数,最大 64)</param>
|
||||
/// <param name="ADataArray">报文数据数组,超过 64 字节的部分会被截断</param>
|
||||
/// <param name="AIsFD">是否为 CANFD 帧,默认 true</param>
|
||||
/// <param name="AIsBRS">是否切换数据段波特率(仅 CANFD 有效),默认 false</param>
|
||||
/// <param name="isUpdateDBCDatabase">是否同步更新 DBC 数据库中同帧 ID 报文的数据,默认 false</param>
|
||||
/// <param name="sendPeriod">发送周期(毫秒):0 = 只发送一次;>0 = 按该周期循环发送</param>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
public static int SendMsg(APP_CHANNEL AIdxChn, int AID, bool AIsTx, bool AIsExt,
|
||||
bool AIsRemote, byte ADLC, byte[] ADataArray, bool AIsFD = true, bool AIsBRS = false,
|
||||
bool isUpdateDBCDatabase = false, float sendPeriod = 0)
|
||||
@@ -635,12 +380,12 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加循环发送报文
|
||||
/// 将 DBC 中定义的报文加入循环发送列表,按指定周期持续发送。
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <param name="AMsgName"></param>
|
||||
/// <param name="sendPeriod"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="channel">通道号(0 起始)</param>
|
||||
/// <param name="AMsgName">DBC 中的报文名称</param>
|
||||
/// <param name="sendPeriod">发送周期(毫秒)</param>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
public static int AddCyclicMsg(byte channel, string AMsgName, float sendPeriod)
|
||||
{
|
||||
var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
|
||||
@@ -649,19 +394,19 @@ namespace TSMasterCAN
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除循环发送的报文
|
||||
/// 清除所有循环发送的报文,停止全部周期发送任务。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>返回代码,0 表示成功,非 0 表示失败</returns>
|
||||
public static int DeleteCyclicMsgs()
|
||||
{
|
||||
return TsMasterApi.tsapp_delete_cyclic_msgs();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取错误提示
|
||||
/// 根据错误代码获取对应的错误文字描述,用于诊断 TSMaster API 返回码。
|
||||
/// </summary>
|
||||
/// <param name="errorCode">错误代码</param>
|
||||
/// <returns></returns>
|
||||
/// <param name="errorCode">TSMaster API 返回的错误代码</param>
|
||||
/// <returns>错误描述文本,未知错误代码时返回提示信息</returns>
|
||||
public static string GetErrorDescription(int errorCode)
|
||||
{
|
||||
IntPtr ADesc = IntPtr.Zero;
|
||||
|
||||
@@ -120,44 +120,8 @@ namespace TSMasterCAN
|
||||
NODE_RX_CANFD_MESSAGE_INDEX = 110,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBC 解码后的信号信息(替代原 ZLG ZDBC.DBCSignal)。
|
||||
/// </summary>
|
||||
public struct DBCSignal
|
||||
{
|
||||
/// <summary>信号名称(原始字节,兼容 Encoding.Default 解码)</summary>
|
||||
public byte[] strName;
|
||||
/// <summary>原始值(TSMaster tsdb_get_signal_value_canfd 返回的物理值)</summary>
|
||||
public double nRawvalue;
|
||||
/// <summary>因子(TSMaster 返回物理值,因子固定为 1)</summary>
|
||||
public double nFactor;
|
||||
/// <summary>偏移(TSMaster 返回物理值,偏移固定为 0)</summary>
|
||||
public double nOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBC 解码后的报文信息(替代原 ZLG ZDBC.DBCMessage)。
|
||||
/// 在 CAN 接收回调中对原始 TLIBCANFD 帧解码后构建此对象并触发 OnDbcMessageDecoded 事件。
|
||||
/// </summary>
|
||||
public class DBCMessage
|
||||
{
|
||||
/// <summary>报文 ID</summary>
|
||||
public uint nID;
|
||||
/// <summary>信号数量</summary>
|
||||
public int nSignalCount;
|
||||
/// <summary>信号数组</summary>
|
||||
public DBCSignal[] vSignals;
|
||||
}
|
||||
|
||||
public class DBCParse
|
||||
{
|
||||
private static DBCParse? _instance;
|
||||
/// <summary>单例实例,供 CAN.DBCParser 属性使用(MsgDatabase 等成员为 static,实例仅用于属性访问兼容)。</summary>
|
||||
public static DBCParse Instance => _instance ??= new DBCParse();
|
||||
|
||||
/// <summary>最大通道数(等于 MsgDatabase 的维度)</summary>
|
||||
public int MaxChannels => MsgDatabase.Count;
|
||||
|
||||
public static List<List<_Msg_>> MsgDatabase { get; set; } =
|
||||
[.. Enumerable.Range(0, 12).Select(_ => new List<_Msg_>())];
|
||||
public static can_network can_Network;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<UseWPF>false</UseWPF>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -192,21 +192,26 @@ namespace TestingModule.ViewModels
|
||||
var assembly = Assembly.LoadFrom(dllPath);
|
||||
Assemblies.Add(assembly);
|
||||
|
||||
// 加载对应的XML注释文件 (项目没有用到)
|
||||
//string xmlPath = Path.ChangeExtension(dllPath, ".xml");
|
||||
//if (File.Exists(xmlPath))
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// XmlDocument xmlDoc = new XmlDocument();
|
||||
// xmlDoc.Load(xmlPath);
|
||||
// _xmlDocumentCache[assembly.FullName!] = xmlDoc;
|
||||
// }
|
||||
// catch (Exception xmlEx)
|
||||
// {
|
||||
// LoggerHelper.WarnWithNotify($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}");
|
||||
// }
|
||||
//}
|
||||
//加载对应的XML注释文件
|
||||
string xmlPath = Path.ChangeExtension(dllPath, ".xml");
|
||||
if (File.Exists(xmlPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
XmlDocument xmlDoc = new XmlDocument();
|
||||
xmlDoc.Load(xmlPath);
|
||||
XmlDocumentCache[assembly.FullName!] = xmlDoc;
|
||||
LoggerHelper.Info($"[XML注释] 成功加载: {Path.GetFileName(xmlPath)} -> 程序集 [{assembly.FullName}]");
|
||||
}
|
||||
catch (Exception xmlEx)
|
||||
{
|
||||
LoggerHelper.Warn($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.Warn($"[XML注释] XML文件不存在: {xmlPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -284,6 +289,7 @@ namespace TestingModule.ViewModels
|
||||
{
|
||||
Name = subProgram.Name,
|
||||
Tag = subProgram,
|
||||
Tooltip = subProgram.FilePath
|
||||
});
|
||||
}
|
||||
|
||||
@@ -368,6 +374,7 @@ namespace TestingModule.ViewModels
|
||||
{
|
||||
Name = $"{method.Name}({paramText})",
|
||||
Tag = method,
|
||||
Tooltip = GetMethodDocumentation(method),
|
||||
};
|
||||
|
||||
typeNode.Children.Add(methodNode);
|
||||
@@ -433,6 +440,95 @@ namespace TestingModule.ViewModels
|
||||
}
|
||||
}
|
||||
}
|
||||
// 添加获取注释的方法
|
||||
private string? GetMethodDocumentation(MethodInfo method)
|
||||
{
|
||||
if (method.DeclaringType == null) return null;
|
||||
|
||||
try
|
||||
{
|
||||
string assemblyName = method.DeclaringType.Assembly.FullName!;
|
||||
if (!_xmlDocumentCache.TryGetValue(assemblyName, out XmlDocument? xmlDoc))
|
||||
{
|
||||
LoggerHelper.Warn($"[XML注释] 缓存未命中: 程序集 [{assemblyName}],缓存包含 { _xmlDocumentCache.Count} 个条目: [{string.Join(", ", _xmlDocumentCache.Keys)}]");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成XML文档中的成员ID
|
||||
string memberName = $"M:{method.DeclaringType.FullName}.{method.Name}";
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
memberName += "(" + string.Join(",", parameters.Select(p => p.ParameterType.FullName)) + ")";
|
||||
}
|
||||
|
||||
// 查找注释节点
|
||||
XmlNode? memberNode = xmlDoc.SelectSingleNode($"//member[@name='{memberName}']");
|
||||
if (memberNode == null)
|
||||
{
|
||||
LoggerHelper.Warn($"[XML注释] 节点未找到: {memberName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取摘要(summary)
|
||||
var summaryNode = memberNode.SelectSingleNode("summary");
|
||||
string documentation = "";
|
||||
|
||||
if (summaryNode != null)
|
||||
{
|
||||
documentation += CleanXmlContent(summaryNode.InnerXml);
|
||||
}
|
||||
|
||||
// 获取参数注释(param)
|
||||
var paramNodes = memberNode.SelectNodes("param");
|
||||
if (paramNodes != null && paramNodes.Count > 0)
|
||||
{
|
||||
documentation += "\n\n参数:";
|
||||
foreach (XmlNode paramNode in paramNodes)
|
||||
{
|
||||
string? paramName = paramNode.Attributes?["name"]?.Value;
|
||||
if (!string.IsNullOrEmpty(paramName))
|
||||
{
|
||||
documentation += $"\n • {paramName}: {CleanXmlContent(paramNode.InnerXml)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取返回值注释(returns)
|
||||
var returnsNode = memberNode.SelectSingleNode("returns");
|
||||
if (returnsNode != null)
|
||||
{
|
||||
documentation += $"\n\n返回值: {CleanXmlContent(returnsNode.InnerXml)}";
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(documentation)
|
||||
? null
|
||||
: System.Net.WebUtility.HtmlDecode(documentation.Trim());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Warn($"获取注释失败: {method.Name} - {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法:清理XML内容
|
||||
private string CleanXmlContent(string xmlContent)
|
||||
{
|
||||
return xmlContent
|
||||
.Replace("<see cref=\"", "")
|
||||
.Replace("\"/>", "")
|
||||
.Replace("<para>", "\n")
|
||||
.Replace("</para>", "")
|
||||
.Replace("<seealso", "")
|
||||
.Replace("/>", "")
|
||||
.Replace("<c>", "") // 处理代码标签
|
||||
.Replace("</c>", "")
|
||||
.Replace("<code>", "")
|
||||
.Replace("</code>", "")
|
||||
.Trim();
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region 指令添加
|
||||
|
||||
@@ -568,20 +664,38 @@ namespace TestingModule.ViewModels
|
||||
{
|
||||
// 查找最近的未匹配循环开始
|
||||
StepVM? lastUnmatchedLoopStart = null;
|
||||
for (int i = Program.StepCollection.Count - 1; i >= 0; i--)
|
||||
if (_ScopedContext.SelectedStepList == "主程序")
|
||||
{
|
||||
if (Program.StepCollection[i].StepType == "循环开始")
|
||||
for (int i = Program.StepCollection.Count - 1; i >= 0; i--)
|
||||
{
|
||||
bool isMatched = Program.StepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
|
||||
|
||||
if (!isMatched)
|
||||
if (Program.StepCollection[i].StepType == "循环开始")
|
||||
{
|
||||
lastUnmatchedLoopStart = Program.StepCollection[i];
|
||||
break;
|
||||
bool isMatched = Program.StepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
|
||||
|
||||
if (!isMatched)
|
||||
{
|
||||
lastUnmatchedLoopStart = Program.StepCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = Program.ErrorStepCollection.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Program.ErrorStepCollection[i].StepType == "循环开始")
|
||||
{
|
||||
bool isMatched = Program.ErrorStepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
|
||||
|
||||
if (!isMatched)
|
||||
{
|
||||
lastUnmatchedLoopStart = Program.ErrorStepCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var newStep = new StepVM
|
||||
{
|
||||
Name = "循环结束",
|
||||
|
||||
@@ -16,6 +16,7 @@ using System.Xml;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
using Prism.Events;
|
||||
using Prism.Commands;
|
||||
|
||||
namespace TestingModule.ViewModels
|
||||
{
|
||||
@@ -94,6 +95,30 @@ namespace TestingModule.ViewModels
|
||||
public ICommand DeleteStepCommand { get;set; }
|
||||
public ICommand TabSelectionChangedCommand { get;set; }
|
||||
public ICommand SelectionChangedCommand { get;set; }
|
||||
public ICommand OpenSubProgramCommand { get; set; }
|
||||
public ICommand GoBackCommand { get; set; }
|
||||
public ICommand ToggleTestItemCommand { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 子程序导航(主程序 / 错误程序各自独立)
|
||||
|
||||
// --- 主程序 Tab 导航属性 ---
|
||||
public ProgramVM MainCurrentProgram => _ScopedContext.MainNav.CurrentProgram;
|
||||
public string MainBreadcrumbPath => _ScopedContext.MainNav.BreadcrumbPath;
|
||||
public bool MainCanGoBack => _ScopedContext.MainNav.CanGoBack;
|
||||
public ObservableCollection<StepVM> MainDisplaySteps => _ScopedContext.MainNav.DisplaySteps;
|
||||
|
||||
// --- 错误程序 Tab 导航属性 ---
|
||||
public ProgramVM ErrorCurrentProgram => _ScopedContext.ErrorNav.CurrentProgram;
|
||||
public string ErrorBreadcrumbPath => _ScopedContext.ErrorNav.BreadcrumbPath;
|
||||
public bool ErrorCanGoBack => _ScopedContext.ErrorNav.CanGoBack;
|
||||
public ObservableCollection<StepVM> ErrorDisplaySteps => _ScopedContext.ErrorNav.DisplaySteps;
|
||||
|
||||
/// <summary>
|
||||
/// 当前激活的导航状态(根据选中的 Tab 决定)
|
||||
/// </summary>
|
||||
private ProgramNavigationState ActiveNav => _ScopedContext.ActiveNav;
|
||||
|
||||
#endregion
|
||||
|
||||
public StepsManagerViewModel(IContainerProvider containerProvider, ScopedContext scopedContext, SystemConfig systemConfig, GlobalInfo globalInfo) : base(containerProvider)
|
||||
@@ -108,17 +133,62 @@ namespace TestingModule.ViewModels
|
||||
DeleteStepCommand = new DelegateCommand(DeleteStep);
|
||||
TabSelectionChangedCommand = new DelegateCommand<string>(TabSelectionChanged);
|
||||
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
||||
OpenSubProgramCommand = new DelegateCommand(OpenSubProgram);
|
||||
GoBackCommand = new DelegateCommand(GoBack);
|
||||
ToggleTestItemCommand = new DelegateCommand(ToggleTestItem);
|
||||
SubscribeStepCollections();
|
||||
Program.PropertyChanged += Program_PropertyChanged;
|
||||
Admin = _globalInfo.IsAdmin;
|
||||
|
||||
// 初始化两套导航状态
|
||||
_ScopedContext.MainNav.Initialize(Program, "主程序");
|
||||
_ScopedContext.ErrorNav.Initialize(Program, "错误程序");
|
||||
|
||||
// 订阅主程序导航状态变化 → 转发给 UI
|
||||
_ScopedContext.MainNav.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ProgramNavigationState.CurrentProgram))
|
||||
{
|
||||
RaisePropertyChanged(nameof(MainCurrentProgram));
|
||||
RaisePropertyChanged(nameof(MainDisplaySteps));
|
||||
}
|
||||
else if (e.PropertyName == nameof(ProgramNavigationState.BreadcrumbPath))
|
||||
RaisePropertyChanged(nameof(MainBreadcrumbPath));
|
||||
else if (e.PropertyName == nameof(ProgramNavigationState.CanGoBack))
|
||||
RaisePropertyChanged(nameof(MainCanGoBack));
|
||||
};
|
||||
|
||||
// 订阅错误程序导航状态变化 → 转发给 UI
|
||||
_ScopedContext.ErrorNav.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ProgramNavigationState.CurrentProgram))
|
||||
{
|
||||
RaisePropertyChanged(nameof(ErrorCurrentProgram));
|
||||
RaisePropertyChanged(nameof(ErrorDisplaySteps));
|
||||
}
|
||||
else if (e.PropertyName == nameof(ProgramNavigationState.BreadcrumbPath))
|
||||
RaisePropertyChanged(nameof(ErrorBreadcrumbPath));
|
||||
else if (e.PropertyName == nameof(ProgramNavigationState.CanGoBack))
|
||||
RaisePropertyChanged(nameof(ErrorCanGoBack));
|
||||
};
|
||||
|
||||
// 订阅子程序导航事件(运行时由 StepRunning 发布)
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>()
|
||||
.Subscribe(OnSubProgramNavigate, ThreadOption.UIThread, false,
|
||||
payload => payload.Scope == _systemConfig.Title);
|
||||
}
|
||||
|
||||
private void SelectionChanged(object parameter)
|
||||
{
|
||||
var selectedList = parameter as IList;
|
||||
if (selectedList != null)
|
||||
if (parameter is IList list && list.Count > 0)
|
||||
{
|
||||
SelectedItems = selectedList.Cast<StepVM>().ToList();
|
||||
SelectedItems = list.Cast<StepVM>().ToList();
|
||||
}
|
||||
else if (parameter is IEnumerable enumerable && parameter is not string)
|
||||
{
|
||||
var items = enumerable.Cast<StepVM>().ToList();
|
||||
if (items.Count > 0)
|
||||
SelectedItems = items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,13 +207,19 @@ namespace TestingModule.ViewModels
|
||||
}
|
||||
private void CopyStep()
|
||||
{
|
||||
if (_globalInfo.IsAdmin && SelectedItems.Any())
|
||||
if (!_globalInfo.IsAdmin) return;
|
||||
|
||||
// 优先用多选列表,回退到单选
|
||||
var source = (SelectedItems != null && SelectedItems.Any())
|
||||
? SelectedItems
|
||||
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||
|
||||
if (source == null || !source.Any()) return;
|
||||
|
||||
tmpCopyList.Clear();
|
||||
foreach (var item in source)
|
||||
{
|
||||
tmpCopyList.Clear();
|
||||
foreach (var item in SelectedItems)
|
||||
{
|
||||
tmpCopyList.Add(item);
|
||||
}
|
||||
tmpCopyList.Add(item);
|
||||
}
|
||||
}
|
||||
private void PasteStep()
|
||||
@@ -180,32 +256,116 @@ namespace TestingModule.ViewModels
|
||||
}
|
||||
private void DeleteStep()
|
||||
{
|
||||
// 确保有选中的项
|
||||
if (_globalInfo.IsAdmin && SelectedItems != null && SelectedItems.Any())
|
||||
if (!_globalInfo.IsAdmin) return;
|
||||
|
||||
// 优先用多选列表,回退到单选
|
||||
var source = (SelectedItems != null && SelectedItems.Any())
|
||||
? SelectedItems.ToList()
|
||||
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||
|
||||
if (source == null || !source.Any()) return;
|
||||
|
||||
foreach (var item in source)
|
||||
{
|
||||
// 创建一个副本进行循环,防止在 Remove 过程中集合变化导致的问题
|
||||
var toDelete = SelectedItems.ToList();
|
||||
_eventAggregator.GetEvent<DeletedStepEvent>().Publish(item.ID);
|
||||
|
||||
foreach (var item in toDelete)
|
||||
{
|
||||
_eventAggregator.GetEvent<DeletedStepEvent>().Publish(item.ID);
|
||||
if (_ScopedContext.SelectedStepList == "主程序")
|
||||
Program.StepCollection.Remove(item);
|
||||
else if (_ScopedContext.SelectedStepList == "错误程序")
|
||||
Program.ErrorStepCollection.Remove(item);
|
||||
}
|
||||
|
||||
if (_ScopedContext.SelectedStepList == "主程序")
|
||||
{
|
||||
Program.StepCollection.Remove(item);
|
||||
}
|
||||
else if (_ScopedContext.SelectedStepList == "错误程序")
|
||||
{
|
||||
Program.ErrorStepCollection.Remove(item);
|
||||
}
|
||||
}
|
||||
// 清空 ViewModel 的选中状态,避免悬挂引用
|
||||
SelectedStep = null;
|
||||
SelectedItems?.Clear();
|
||||
_ScopedContext.SelectedStep = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
// 3. 清空 ViewModel 的选中状态,避免悬挂引用
|
||||
SelectedStep = null;
|
||||
SelectedItems.Clear();
|
||||
_ScopedContext.SelectedStep = null;
|
||||
#region 子程序导航方法
|
||||
|
||||
/// <summary>
|
||||
/// 编辑模式:右键打开子程序(操作当前激活的 Tab 的导航状态)
|
||||
/// </summary>
|
||||
private void OpenSubProgram()
|
||||
{
|
||||
if (SelectedStep == null || SelectedStep.StepType != "子程序" || SelectedStep.SubProgram == null)
|
||||
return;
|
||||
|
||||
var nav = ActiveNav;
|
||||
nav.NavigationStack.Push(nav.CurrentProgram);
|
||||
nav.UpdateCanGoBack();
|
||||
var stepName = SelectedStep.Name ?? "子程序";
|
||||
nav.BreadcrumbPath = $"{nav.BreadcrumbPath} > {stepName}";
|
||||
nav.CurrentProgram = SelectedStep.SubProgram;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换选中步骤的测试项标记(仅子程序可标记,运行时将记录其范围内所有 OKExpression 判断)
|
||||
/// </summary>
|
||||
private void ToggleTestItem()
|
||||
{
|
||||
if (!_globalInfo.IsAdmin) return;
|
||||
|
||||
var source = (SelectedItems != null && SelectedItems.Any())
|
||||
? SelectedItems
|
||||
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||
|
||||
if (source == null || !source.Any()) return;
|
||||
|
||||
foreach (var item in source.Where(x => x.StepType == "子程序" && x.SubProgram != null))
|
||||
{
|
||||
item.IsTestItem = !item.IsTestItem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回上一级程序(操作当前激活的 Tab 的导航状态)
|
||||
/// </summary>
|
||||
private void GoBack()
|
||||
{
|
||||
var nav = ActiveNav;
|
||||
if (nav.NavigationStack.Count == 0)
|
||||
return;
|
||||
|
||||
var parentProgram = nav.NavigationStack.Pop();
|
||||
nav.UpdateCanGoBack();
|
||||
nav.CurrentProgram = parentProgram;
|
||||
|
||||
var parts = nav.BreadcrumbPath.Split(" > ");
|
||||
nav.BreadcrumbPath = parts.Length > 1
|
||||
? string.Join(" > ", parts.Take(parts.Length - 1))
|
||||
: nav == _ScopedContext.MainNav ? "主程序" : "错误程序";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行时:处理 StepRunning 发布的子程序导航事件
|
||||
/// </summary>
|
||||
private void OnSubProgramNavigate(SubProgramNavigatePayload payload)
|
||||
{
|
||||
if (_ScopedContext == null || payload == null) return;
|
||||
var nav = payload.IsErrorProgram ? _ScopedContext.ErrorNav : _ScopedContext.MainNav;
|
||||
if (payload.Action == NavigateAction.Enter && payload.SubProgram != null)
|
||||
{
|
||||
nav.NavigationStack.Push(nav.CurrentProgram);
|
||||
nav.UpdateCanGoBack();
|
||||
nav.CurrentProgram = payload.SubProgram;
|
||||
var stepName = payload.StepName ?? "子程序";
|
||||
nav.BreadcrumbPath = $"{nav.BreadcrumbPath} > {stepName}";
|
||||
}
|
||||
else if (payload.Action == NavigateAction.Exit)
|
||||
{
|
||||
if (nav.NavigationStack.Count == 0) return;
|
||||
var parentProgram = nav.NavigationStack.Pop();
|
||||
nav.UpdateCanGoBack();
|
||||
nav.CurrentProgram = parentProgram;
|
||||
var parts = nav.BreadcrumbPath.Split(" > ");
|
||||
nav.BreadcrumbPath = parts.Length > 1
|
||||
? string.Join(" > ", parts.Take(parts.Length - 1))
|
||||
: nav == _ScopedContext.MainNav ? "主程序" : "错误程序";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助方法
|
||||
@@ -252,6 +412,12 @@ namespace TestingModule.ViewModels
|
||||
UnsubscribeStepCollections();
|
||||
SubscribeStepCollections();
|
||||
|
||||
// 通知 UI 刷新 DisplaySteps(导航状态虽然引用同一 Program,但集合实例已变)
|
||||
RaisePropertyChanged(nameof(MainDisplaySteps));
|
||||
RaisePropertyChanged(nameof(ErrorDisplaySteps));
|
||||
RaisePropertyChanged(nameof(MainCurrentProgram));
|
||||
RaisePropertyChanged(nameof(ErrorCurrentProgram));
|
||||
|
||||
// 集合被整体替换后,对新集合重新编号
|
||||
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
@@ -304,9 +470,9 @@ namespace TestingModule.ViewModels
|
||||
Program.PropertyChanged -= Program_PropertyChanged;
|
||||
}
|
||||
|
||||
// 3. 【核心修复】必须显式退订 Prism 全局事件(AlarmEvent)
|
||||
// 注意:因为订阅时使用的是匿名 Lambda,最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销
|
||||
// 3. 显式退订全局 Prism 事件
|
||||
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
|
||||
_eventAggregator?.GetEvent<SubProgramNavigateEvent>()?.Unsubscribe(null);
|
||||
|
||||
// 4. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
|
||||
tmpCopyList?.Clear();
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type model:InstructionNodeVM}"
|
||||
ItemsSource="{Binding Children}">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text="{Binding Name}" ToolTip="{Binding Tooltip}"/>
|
||||
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
<!-- 双击 -->
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="100" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
@@ -240,7 +240,7 @@
|
||||
<DataTemplate>
|
||||
<Grid Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Item}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="100" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
xmlns:behaviors="clr-namespace:UIShare.Behaviors;assembly=UIShare"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<GroupBox Header="{Binding Title}">
|
||||
<TabControl SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">
|
||||
@@ -19,6 +23,17 @@
|
||||
CommandParameter="{Binding SelectedTabHeader}" />
|
||||
</i:Interaction.Behaviors>
|
||||
<TabItem Header="主程序">
|
||||
<DockPanel>
|
||||
<!-- 主程序面包屑导航栏 -->
|
||||
<Border DockPanel.Dock="Top" Background="#F0F0F0" Padding="6,3" Visibility="{Binding MainCanGoBack, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="↩ 返回上一级"
|
||||
Command="{Binding GoBackCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
FontSize="12" Padding="8,2" Margin="8,0,0,0"/>
|
||||
<TextBlock Text="{Binding MainBreadcrumbPath}" VerticalAlignment="Center" FontSize="12" Foreground="#555"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<DataGrid dd:DragDrop.IsDragSource="{Binding Admin}"
|
||||
dd:DragDrop.IsDropTarget="{Binding Admin}"
|
||||
dd:DragDrop.UseDefaultDragAdorner="{Binding Admin}"
|
||||
@@ -29,7 +44,7 @@
|
||||
Background="Transparent"
|
||||
CanUserAddRows="False"
|
||||
CanUserSortColumns="False"
|
||||
ItemsSource="{Binding Program.StepCollection}"
|
||||
ItemsSource="{Binding MainDisplaySteps}"
|
||||
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow">
|
||||
@@ -38,6 +53,10 @@
|
||||
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||
</i:EventTrigger>
|
||||
<i:EventTrigger EventName="ContextMenuOpening">
|
||||
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>
|
||||
<!-- 行样式 -->
|
||||
<DataGrid.RowStyle>
|
||||
@@ -80,6 +99,10 @@
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="启用" />
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsTestItem}"
|
||||
Header="测试项"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn Binding="{Binding Index}"
|
||||
Header="序号"
|
||||
IsReadOnly="True" />
|
||||
@@ -120,14 +143,31 @@
|
||||
<MenuItem Header="删除"
|
||||
Foreground="Red"
|
||||
Command="{Binding DeleteStepCommand}" />
|
||||
<Separator/>
|
||||
<MenuItem Header="打开子程序"
|
||||
Command="{Binding OpenSubProgramCommand}" />
|
||||
<MenuItem Header="标记/取消测试项"
|
||||
Command="{Binding ToggleTestItemCommand}" />
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
|
||||
|
||||
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="错误程序">
|
||||
<DockPanel>
|
||||
<!-- 错误程序面包屑导航栏 -->
|
||||
<Border DockPanel.Dock="Top" Background="#F0F0F0" Padding="6,3" Visibility="{Binding ErrorCanGoBack, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="↩ 返回上一级"
|
||||
Command="{Binding GoBackCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
FontSize="12" Padding="8,2" Margin="8,0,0,0"/>
|
||||
<TextBlock Text="{Binding ErrorBreadcrumbPath}" VerticalAlignment="Center" FontSize="12" Foreground="#555"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<DataGrid dd:DragDrop.IsDragSource="True"
|
||||
dd:DragDrop.IsDropTarget="True"
|
||||
dd:DragDrop.UseDefaultDragAdorner="True"
|
||||
@@ -135,7 +175,7 @@
|
||||
Background="Transparent"
|
||||
CanUserAddRows="False"
|
||||
CanUserSortColumns="False"
|
||||
ItemsSource="{Binding Program.ErrorStepCollection}"
|
||||
ItemsSource="{Binding ErrorDisplaySteps}"
|
||||
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow">
|
||||
@@ -144,6 +184,10 @@
|
||||
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||
</i:EventTrigger>
|
||||
<i:EventTrigger EventName="ContextMenuOpening">
|
||||
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>
|
||||
<!-- 行样式 -->
|
||||
<DataGrid.RowStyle>
|
||||
@@ -186,6 +230,10 @@
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="启用" />
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsTestItem}"
|
||||
Header="测试项"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn Binding="{Binding Index}"
|
||||
Header="序号"
|
||||
IsReadOnly="True" />
|
||||
@@ -226,6 +274,11 @@
|
||||
<MenuItem Header="删除"
|
||||
Foreground="Red"
|
||||
Command="{Binding DeleteStepCommand}" />
|
||||
<Separator/>
|
||||
<MenuItem Header="打开子程序"
|
||||
Command="{Binding OpenSubProgramCommand}" />
|
||||
<MenuItem Header="标记/取消测试项"
|
||||
Command="{Binding ToggleTestItemCommand}" />
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
|
||||
@@ -239,6 +292,7 @@
|
||||
</i:Interaction.Triggers>-->
|
||||
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</GroupBox>
|
||||
|
||||
@@ -13,18 +13,20 @@ namespace UIShare.Converters
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// 是否反转结果(字符串非空时返回 Collapsed),用于需要“有值则隐藏”的场景,如展开全屏时隐藏台架标签页
|
||||
bool inverse = string.Equals(parameter?.ToString(), "Inverse", StringComparison.OrdinalIgnoreCase);
|
||||
if(value is string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
return inverse ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Visibility.Visible;
|
||||
return inverse ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
}
|
||||
return Visibility.Collapsed;
|
||||
return inverse ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TSMasterCAN;
|
||||
using TSMaster;
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public class CANMonitoringService
|
||||
{
|
||||
public bool IsStopped { get; set; } = false;
|
||||
public bool IsInitialized { get; set; } = false;
|
||||
public bool IsRegistered { get; set; } = false;
|
||||
private SystemConfig _systemConfig { get; set; }
|
||||
public CANMonitoringService(SystemConfig systemConfig)
|
||||
{
|
||||
_systemConfig = systemConfig;
|
||||
CANSignalBroadcaster.RegisterService(this);
|
||||
IsRegistered = true;
|
||||
}
|
||||
public int Init(CancellationToken ct = default)
|
||||
{
|
||||
int re = 0;
|
||||
if (IsInitialized == false)
|
||||
{
|
||||
re = CAN.Init(_systemConfig.TSMasterName);
|
||||
}
|
||||
if (IsInitialized == false)
|
||||
{
|
||||
re = CAN.RegisterListener(MonitorEvent);
|
||||
if (re == 0) IsInitialized = true;
|
||||
}
|
||||
return re;
|
||||
}
|
||||
public ConcurrentDictionary<string, TLIBCANFD> RealTimeMessages { get; set; } = new();
|
||||
public ConcurrentDictionary<string, double> RealTimeSignals { get; set; } = new();
|
||||
public void MonitorEvent(ref int AObj, ref TLIBCANFD AData)
|
||||
{
|
||||
if (IsStopped) return;
|
||||
byte[] oldbytes = null;
|
||||
|
||||
//记录至实时报文数据库
|
||||
var cpAData = AData;
|
||||
cpAData.FData = ArrayPool<byte>.Shared.Rent(64);
|
||||
Array.Copy(AData.FData, 0, cpAData.FData, 0, cpAData.FData.Length);
|
||||
var MessageName = $"{AData.FIdxChn}/{AData.FIdentifier}";
|
||||
if (RealTimeMessages.TryGetValue(MessageName, out var v))
|
||||
{
|
||||
oldbytes = v.FData;
|
||||
}
|
||||
RealTimeMessages[MessageName] = cpAData;
|
||||
|
||||
//记录至DBC数据库
|
||||
var dbcfind = DBCParse.MsgDatabase[AData.FIdxChn].FirstOrDefault(s => s.msg_id == cpAData.FIdentifier);
|
||||
if (dbcfind != null)
|
||||
{
|
||||
Array.Copy(cpAData.FData, 0, dbcfind.ACANFD.FData, 0, cpAData.FData.Length);
|
||||
dbcfind.ACANFD.FTimeUS = cpAData.FTimeUS;
|
||||
dbcfind.ACANFD.FIsFD = cpAData.FIsFD;
|
||||
dbcfind.ACANFD.FIsBRS = cpAData.FIsBRS;
|
||||
dbcfind.ACANFD.FIsESI = cpAData.FIsESI;
|
||||
dbcfind.ACANFD.FIsError = cpAData.FIsError;
|
||||
dbcfind.ACANFD.FIsRemote = cpAData.FIsRemote;
|
||||
dbcfind.ACANFD.FIsExt = cpAData.FIsExt;
|
||||
}
|
||||
|
||||
if (oldbytes is not null) ArrayPool<byte>.Shared.Return(oldbytes);
|
||||
|
||||
var id = AData.FIdentifier;
|
||||
var ch = AData.FIdxChn;
|
||||
|
||||
var canSignalList = _systemConfig.ConfigurationList.Where(s => s.MessageID == id && s.Channel == ch).ToArray();
|
||||
|
||||
if (canSignalList.Length > 0)
|
||||
{
|
||||
var find = DBCParse.MsgDatabase[ch].First(s => s.msg_id == id);
|
||||
foreach (var item in canSignalList)
|
||||
{
|
||||
double Signalre = double.NaN;
|
||||
var re = CAN.GetSignalValue(ref cpAData, item.MessageName, item.SignalName, ref Signalre);
|
||||
//Debug.Assert(re == 0);
|
||||
var SinalName = $"{AData.FIdxChn}/{item.MessageName}/{item.SignalName}";
|
||||
RealTimeSignals[SinalName] = Signalre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止监测:从广播器注销并清理实时数据。
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
IsStopped = true;
|
||||
if (IsRegistered)
|
||||
{
|
||||
CANSignalBroadcaster.UnregisterService(this);
|
||||
IsRegistered = false;
|
||||
}
|
||||
RealTimeSignals.Clear();
|
||||
RealTimeMessages.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +1,229 @@
|
||||
using Model.Models;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using TSMasterCAN;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// CAN 信号广播器(全局单例):
|
||||
/// 统一订阅 GlobalInfo.CanPool 中所有已实例化的 ZLGCANFD 设备,
|
||||
/// 将 DBC 解码后的信号值广播给所有引用该 CAN 设备的作用域,
|
||||
/// 并同步检查各作用域 ValueLimitList 的超限报警。
|
||||
/// CAN 信号广播器(全局单例,支持多作用域):
|
||||
/// 从所有已注册的 <see cref="CANMonitoringService"/> 的 RealTimeSignals 字典中轮询读取信号值,
|
||||
/// 按每个已注册作用域的 ConfigurationList 分别映射,向对应作用域广播 HardwareDataReportedEvent。
|
||||
/// <para>
|
||||
/// 一拖三场景下 CAN 硬件共享,但各工位的 DBC/ConfigurationList 不同,
|
||||
/// 因此广播器必须遍历所有已注册作用域的配置分别广播。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CANSignalBroadcaster : IDisposable
|
||||
public class CANSignalBroadcaster
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>已订阅解码事件的 CANFD 实例 → 委托 映射</summary>
|
||||
private readonly Dictionary<CAN, Action<uint, DBCMessage>> _handlers = new();
|
||||
public CANSignalBroadcaster(GlobalInfo globalInfo, IEventAggregator eventAggregator)
|
||||
/// <summary>
|
||||
/// 作用域注册表:scopeName → (SystemConfig, 预构建的 ConfigMap)。
|
||||
/// 每个工位的 MonitorViewModel 在初始化时调用 <see cref="RegisterScope"/> 注册自己的配置。
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, (SystemConfig Config, Dictionary<string, CANSignalConfig> ConfigMap)> _scopeRegistry = new();
|
||||
|
||||
/// <summary>全局已注册的 CANMonitoringService 实例列表(静态,跨作用域共享)</summary>
|
||||
private static readonly ConcurrentDictionary<CANMonitoringService, byte> _registeredServices = new();
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _broadcastTask;
|
||||
|
||||
/// <summary>广播轮询间隔(毫秒)</summary>
|
||||
public int PollingIntervalMs { get; set; } = 100;
|
||||
|
||||
public CANSignalBroadcaster(SystemConfig systemConfig, IEventAggregator eventAggregator)
|
||||
{
|
||||
_globalInfo = globalInfo;
|
||||
_eventAggregator = eventAggregator;
|
||||
// 构造时自动注册第一个作用域(向后兼容)
|
||||
RegisterScope(systemConfig.Title, systemConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扫描 GlobalInfo.CanPool 中已创建的 CANFD 实例并订阅解码事件。
|
||||
/// 幂等:已订阅的实例不会重复订阅。
|
||||
/// </summary>
|
||||
public void Discover()
|
||||
#region 服务注册(供 CANMonitoringService 调用)
|
||||
|
||||
/// <summary>注册一个 CANMonitoringService,使其信号纳入广播</summary>
|
||||
public static void RegisterService(CANMonitoringService service)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
foreach (var kvp in _globalInfo.CanPool)
|
||||
{
|
||||
string fingerprint = kvp.Key;
|
||||
var lazy = kvp.Value;
|
||||
if (!lazy.IsValueCreated || lazy.Value == null) continue;
|
||||
|
||||
var canfd = lazy.Value;
|
||||
if (_handlers.ContainsKey(canfd)) continue;
|
||||
|
||||
// 使用闭包捕获指纹,回调时即可区分不同 CAN 卡
|
||||
Action<uint, DBCMessage> handler = (channel, msg) => OnDbcMessageDecoded(fingerprint, channel, msg);
|
||||
canfd.OnDbcMessageDecoded += handler;
|
||||
_handlers[canfd] = handler;
|
||||
}
|
||||
_registeredServices.TryAdd(service, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动广播器(目前 Discover 已完成订阅,此处保留以兼容 HardwareDataBroadcaster 的使用模式)。
|
||||
/// </summary>
|
||||
/// <summary>注销一个 CANMonitoringService</summary>
|
||||
public static void UnregisterService(CANMonitoringService service)
|
||||
{
|
||||
_registeredServices.TryRemove(service, out _);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 启动 / 停止
|
||||
|
||||
/// <summary>启动广播(幂等:多次调用只启动一次)</summary>
|
||||
public void Start()
|
||||
{
|
||||
Discover();
|
||||
if (_broadcastTask != null && !_broadcastTask.IsCompleted) return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_broadcastTask = Task.Run(() => BroadcastLoop(_cts.Token));
|
||||
}
|
||||
|
||||
/// <summary>停止广播</summary>
|
||||
public void Stop()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_broadcastTask = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBC 解码回调:提取所有信号,向引用该 CAN 设备的作用域广播,并检查报警。
|
||||
/// 注册一个作用域的 CAN 配置。
|
||||
/// 每个工位在 MonitorViewModel 初始化时调用此方法,将自己的 SystemConfig 注册进来,
|
||||
/// 广播器会在每轮广播中为该作用域独立映射信号并广播。
|
||||
/// </summary>
|
||||
private void OnDbcMessageDecoded(string canFingerprint, uint channel, DBCMessage msg)
|
||||
/// <param name="scopeName">作用域名称(SystemConfig.Title)</param>
|
||||
/// <param name="config">该作用域的 SystemConfig</param>
|
||||
public void RegisterScope(string scopeName, SystemConfig config)
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (string.IsNullOrEmpty(scopeName) || config == null) return;
|
||||
var configMap = BuildConfigMap(config);
|
||||
_scopeRegistry.AddOrUpdate(scopeName, (config, configMap), (_, _) => (config, configMap));
|
||||
}
|
||||
|
||||
string signalFingerprint = BuildFingerprint(canFingerprint, channel);
|
||||
var now = DateTime.Now;
|
||||
/// <summary>注销一个作用域(工位销毁时调用)</summary>
|
||||
public void UnregisterScope(string scopeName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(scopeName))
|
||||
_scopeRegistry.TryRemove(scopeName, out _);
|
||||
}
|
||||
|
||||
// 获取引用该 CAN 设备的所有作用域
|
||||
var scopes = GetScopesForFingerprint(canFingerprint);
|
||||
if (scopes.Count == 0) return;
|
||||
/// <summary>兼容旧接口:Discover 已无需执行任何操作</summary>
|
||||
public void Discover() { }
|
||||
|
||||
for (int i = 0; i < msg.nSignalCount; i++)
|
||||
#endregion
|
||||
|
||||
#region 广播核心逻辑
|
||||
|
||||
/// <summary>
|
||||
/// 轮询所有已注册的 CANMonitoringService 的 RealTimeSignals,
|
||||
/// 遍历所有已注册作用域的配置分别映射并广播 HardwareDataReportedEvent。
|
||||
/// </summary>
|
||||
private async Task BroadcastLoop(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var signal = msg.vSignals[i];
|
||||
string signalName = Encoding.Default.GetString(signal.strName).TrimEnd('\0');
|
||||
if (string.IsNullOrEmpty(signalName)) continue;
|
||||
|
||||
double physicalValue = signal.nRawvalue * signal.nFactor + signal.nOffset;
|
||||
string methodName = BuildMethodName(msg.nID, signalName);
|
||||
|
||||
foreach (var scope in scopes)
|
||||
try
|
||||
{
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
|
||||
{
|
||||
Scope = scope,
|
||||
HardwareFingerprint = signalFingerprint,
|
||||
MethodName = methodName,
|
||||
Value = physicalValue,
|
||||
Time = now
|
||||
});
|
||||
// 快照当前作用域注册表,避免枚举期间被修改
|
||||
var scopeSnapshot = _scopeRegistry.ToArray();
|
||||
|
||||
string MonitorStatus = ValueLimitAlarmHelper.CheckAlarm(scope, signalFingerprint, methodName, physicalValue, _globalInfo);
|
||||
if (MonitorStatus != "" && MonitorStatus != "未报警")
|
||||
foreach (var scopeEntry in scopeSnapshot)
|
||||
{
|
||||
_eventAggregator.GetEvent<AlarmEvent>().Publish((scope,canFingerprint, MonitorStatus));
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
string scopeName = scopeEntry.Key;
|
||||
var scopeConfig = scopeEntry.Value.Config;
|
||||
var configMap = scopeEntry.Value.ConfigMap;
|
||||
|
||||
foreach (var service in _registeredServices.Keys)
|
||||
{
|
||||
if (service.IsStopped) continue;
|
||||
|
||||
foreach (var kvp in service.RealTimeSignals)
|
||||
{
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
// 信号 Key 格式: "{channel}/{MessageName}/{SignalName}"
|
||||
if (!TryParseSignalKey(kvp.Key, out int channel, out string? messageName, out string? signalName))
|
||||
continue;
|
||||
|
||||
// 从该作用域的配置映射中查找对应的 MessageID
|
||||
if (!configMap.TryGetValue($"{channel}/{messageName}/{signalName}", out var cfg))
|
||||
continue;
|
||||
|
||||
string canFingerprint = $"CAN:{channel}";
|
||||
string fingerprint = BuildFingerprint(canFingerprint, (uint)channel);
|
||||
string methodName = BuildMethodName((uint)cfg.MessageID, signalName);
|
||||
|
||||
// 广播信号值到对应作用域
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
|
||||
{
|
||||
Scope = scopeName,
|
||||
HardwareFingerprint = fingerprint,
|
||||
MethodName = methodName,
|
||||
Value = kvp.Value,
|
||||
Time = DateTime.Now
|
||||
});
|
||||
|
||||
// 报警检查(使用该作用域自己的配置)
|
||||
string alarmStatus = ValueLimitAlarmHelper.CheckAlarm(fingerprint, methodName, kvp.Value, scopeConfig);
|
||||
if (!string.IsNullOrEmpty(alarmStatus) && alarmStatus != "未报警")
|
||||
{
|
||||
_eventAggregator.GetEvent<AlarmEvent>().Publish((scopeName, canFingerprint, alarmStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(PollingIntervalMs, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LoggerHelper.Error($"CANSignalBroadcaster 广播异常: {ex.Message}");
|
||||
await Task.Delay(1000, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取指定硬件指纹当前被哪些作用域引用</summary>
|
||||
private List<string> GetScopesForFingerprint(string fingerprint)
|
||||
/// <summary>
|
||||
/// 从 SystemConfig.ConfigurationList 构建 "channel/MessageName/SignalName" → CANSignalConfig 的映射
|
||||
/// </summary>
|
||||
private static Dictionary<string, CANSignalConfig> BuildConfigMap(SystemConfig systemConfig)
|
||||
{
|
||||
if (_globalInfo.DeviceAndScopeDic.TryGetValue(fingerprint, out var lazy))
|
||||
var map = new Dictionary<string, CANSignalConfig>(StringComparer.OrdinalIgnoreCase);
|
||||
if (systemConfig?.ConfigurationList == null) return map;
|
||||
|
||||
foreach (var cfg in systemConfig.ConfigurationList)
|
||||
{
|
||||
var scopeList = lazy.Value;
|
||||
lock (scopeList) return scopeList.ToList();
|
||||
if (string.IsNullOrEmpty(cfg.SignalName) || string.IsNullOrEmpty(cfg.MessageName)) continue;
|
||||
string key = $"{cfg.Channel}/{cfg.MessageName}/{cfg.SignalName}";
|
||||
map.TryAdd(key, cfg); // 第一个匹配的优先
|
||||
}
|
||||
return new List<string>();
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CAN 信号的 DisplayName 格式:"{MessageName}.{SignalName}"
|
||||
/// </summary>
|
||||
/// <summary>解析信号 Key: "{channel}/{MessageName}/{SignalName}"</summary>
|
||||
private static bool TryParseSignalKey(string key, out int channel, out string? messageName, out string? signalName)
|
||||
{
|
||||
channel = 0;
|
||||
messageName = null;
|
||||
signalName = null;
|
||||
|
||||
var parts = key.Split('/');
|
||||
if (parts.Length < 3) return false;
|
||||
|
||||
if (!int.TryParse(parts[0], out channel)) return false;
|
||||
messageName = parts[1];
|
||||
signalName = parts[2];
|
||||
return !string.IsNullOrEmpty(messageName) && !string.IsNullOrEmpty(signalName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 静态工具方法(保持向后兼容)
|
||||
|
||||
/// <summary>生成 CAN 信号的 DisplayName 格式:"{MessageName}.{SignalName}"</summary>
|
||||
public static string BuildDisplayName(string messageName, string signalName)
|
||||
{
|
||||
return $"{messageName}.{signalName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CAN 信号的 MethodName 格式:"{MessageId:X}.{SignalName}"
|
||||
/// </summary>
|
||||
/// <summary>生成 CAN 信号的 MethodName 格式:"{MessageId:X}.{SignalName}"</summary>
|
||||
public static string BuildMethodName(uint messageId, string signalName)
|
||||
{
|
||||
return $"{messageId:X}.{signalName}";
|
||||
@@ -140,17 +238,6 @@ namespace UIShare.GlobalVariable
|
||||
return $"{canDeviceFingerprint}:{channel}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var kvp in _handlers)
|
||||
{
|
||||
if (kvp.Key != null)
|
||||
kvp.Key.OnDbcMessageDecoded -= kvp.Value;
|
||||
}
|
||||
_handlers.Clear();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
using DeviceCommand.Base;
|
||||
using Logger;
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备健康监控器:独立于监控采样的心跳重连机制。
|
||||
/// <para>
|
||||
/// 设计原则——与检测值零冲突:
|
||||
/// <list type="bullet">
|
||||
/// <item>健康检查平时只读 <see cref="IBaseInterface.IsConnected"/>(纯本地属性,零网络开销,不碰通信锁)</item>
|
||||
/// <item>仅在 IsConnected==false 时才获取 _commLock 执行重连,与监控采样天然串行化</item>
|
||||
/// <item>ModbusTcp 的 ConnectAsync 是幂等的(已连接时直接返回),不会中断正在进行的监控</item>
|
||||
/// <item>TCP 的 ConnectAsync 会重置连接,但监控的 catch 容忍单次失败,下次 tick 自动恢复</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class DeviceHealthMonitor : IDisposable
|
||||
{
|
||||
private readonly IDictionary<string, IBaseInterface> _deviceMap;
|
||||
private readonly SystemConfig _systemConfig;
|
||||
private readonly string _scopeName;
|
||||
|
||||
/// <summary>健康检查定时器</summary>
|
||||
private Timer? _healthCheckTimer;
|
||||
|
||||
/// <summary>每个设备的连续失败计数</summary>
|
||||
private readonly ConcurrentDictionary<string, int> _failureCounts = new();
|
||||
|
||||
/// <summary>每个设备的重连尝试次数(用于计算退避阈值)</summary>
|
||||
private readonly ConcurrentDictionary<string, int> _reconnectAttempts = new();
|
||||
|
||||
/// <summary>基础失败阈值(首次重连触发值)</summary>
|
||||
private const int BaseFailureThreshold = 3;
|
||||
|
||||
/// <summary>退避上限(最大阈值)</summary>
|
||||
private const int MaxBackoffThreshold = 15;
|
||||
|
||||
/// <summary>健康检查间隔(毫秒)</summary>
|
||||
private readonly int _checkIntervalMs;
|
||||
|
||||
private bool _disposed;
|
||||
private readonly object _startStopLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 创建健康监控器实例。
|
||||
/// </summary>
|
||||
/// <param name="deviceMap">当前作用域的设备字典</param>
|
||||
/// <param name="systemConfig">当前作用域的系统配置(用于更新 DeviceInfoVM.IsConnected)</param>
|
||||
/// <param name="scopeName">作用域名称(日志标识)</param>
|
||||
/// <param name="checkIntervalMs">健康检查间隔,默认 5000ms</param>
|
||||
public DeviceHealthMonitor(
|
||||
IDictionary<string, IBaseInterface> deviceMap,
|
||||
SystemConfig systemConfig,
|
||||
string scopeName,
|
||||
int checkIntervalMs = 5000)
|
||||
{
|
||||
_deviceMap = deviceMap;
|
||||
_systemConfig = systemConfig;
|
||||
_scopeName = scopeName;
|
||||
_checkIntervalMs = checkIntervalMs;
|
||||
}
|
||||
|
||||
/// <summary>启动健康监控(幂等:多次调用只启动一次)</summary>
|
||||
public void Start()
|
||||
{
|
||||
lock (_startStopLock)
|
||||
{
|
||||
if (_disposed || _healthCheckTimer != null) return;
|
||||
_healthCheckTimer = new Timer(
|
||||
OnHealthCheckTick,
|
||||
null,
|
||||
TimeSpan.FromSeconds(10), // 首次检查延迟 10 秒,避免启动时设备尚未连接完成
|
||||
TimeSpan.FromMilliseconds(_checkIntervalMs));
|
||||
LoggerHelper.Info($"[{_scopeName}] 设备健康监控已启动,检查间隔={_checkIntervalMs}ms,基础重连阈值={BaseFailureThreshold}次(指数退避上限={MaxBackoffThreshold})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>停止健康监控</summary>
|
||||
public void Stop()
|
||||
{
|
||||
lock (_startStopLock)
|
||||
{
|
||||
_healthCheckTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_healthCheckTimer?.Dispose();
|
||||
_healthCheckTimer = null;
|
||||
_failureCounts.Clear();
|
||||
_reconnectAttempts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 健康检查核心逻辑:遍历设备,检查连接状态,失败计数超阈值则重连。
|
||||
/// <para>TCP 设备额外进行主动探活(*IDN?),以检测死连接(对端崩溃但 TCP 未收到 FIN)。</para>
|
||||
/// </summary>
|
||||
private async void OnHealthCheckTick(object? state)
|
||||
{
|
||||
if (_disposed || _deviceMap.Count == 0) return;
|
||||
|
||||
// 快照避免枚举期间字典被修改
|
||||
var snapshot = _deviceMap.ToArray();
|
||||
|
||||
foreach (var kvp in snapshot)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
string deviceName = kvp.Key;
|
||||
var device = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
bool alive = device.IsConnected;
|
||||
|
||||
// TCP 设备主动探活:IsConnected 只反映上次操作状态,无法检测死连接
|
||||
if (alive && device is Tcp tcpDevice)
|
||||
{
|
||||
alive = await ProbeTcpDeviceAsync(tcpDevice);
|
||||
}
|
||||
|
||||
if (alive)
|
||||
{
|
||||
// 连接正常:清零失败计数与退避
|
||||
_failureCounts.TryRemove(deviceName, out _);
|
||||
_reconnectAttempts.TryRemove(deviceName, out _);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 连接断开:累加失败计数
|
||||
int failures = _failureCounts.AddOrUpdate(deviceName, 1, (_, count) => count + 1);
|
||||
|
||||
// 计算当前退避阈值:基础值 + 重连尝试次数 × 2,上限为 MaxBackoffThreshold
|
||||
int attempts = _reconnectAttempts.GetOrAdd(deviceName, 0);
|
||||
int currentThreshold = Math.Min(BaseFailureThreshold + attempts * 2, MaxBackoffThreshold);
|
||||
|
||||
if (failures < currentThreshold)
|
||||
{
|
||||
LoggerHelper.Warn(
|
||||
$"[{_scopeName}] 设备 [{deviceName}] 连接断开,等待重连中 ({failures}/{currentThreshold})");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 达到阈值:执行重连
|
||||
await ReconnectDeviceAsync(deviceName, device);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Error($"[{_scopeName}] 设备 [{deviceName}] 健康检查异常:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对 TCP 设备执行轻量级主动探活(SCPI *IDN?),2 秒超时。
|
||||
/// <para>通过 WriteReadAsync 内部获取 _commLock,与监控采样天然串行化。</para>
|
||||
/// </summary>
|
||||
/// <returns>true: 探活成功(连接确实存活); false: 探活失败(死连接)</returns>
|
||||
private async Task<bool> ProbeTcpDeviceAsync(Tcp tcpDevice)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var probeCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
string resp = await tcpDevice.WriteReadAsync("*IDN?\n", "\n", probeCts.Token);
|
||||
return !string.IsNullOrWhiteSpace(resp);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重连单个设备。
|
||||
/// <para>
|
||||
/// 冲突避免机制:ConnectAsync 内部获取设备的 _commLock,
|
||||
/// 如果此时监控采样正在通信,重连会等待锁释放后再执行,
|
||||
/// 保证同一时刻只有一个操作在使用通信链路。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private async Task ReconnectDeviceAsync(string deviceName, IBaseInterface device)
|
||||
{
|
||||
try
|
||||
{
|
||||
int attempts = _reconnectAttempts.AddOrUpdate(deviceName, 1, (_, c) => c + 1);
|
||||
int nextThreshold = Math.Min(BaseFailureThreshold + attempts * 2, MaxBackoffThreshold);
|
||||
LoggerHelper.Info($"[{_scopeName}] 设备 [{deviceName}] 连续失败触发重连(第 {attempts} 次重连,下次阈值={nextThreshold})...");
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
bool ok = await device.ConnectAsync(cts.Token);
|
||||
|
||||
// 同步更新 DeviceInfoVM 的 UI 状态
|
||||
UpdateDeviceInfoState(deviceName, ok);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
_failureCounts.TryRemove(deviceName, out _);
|
||||
_reconnectAttempts.TryRemove(deviceName, out _);
|
||||
LoggerHelper.Info($"[{_scopeName}] 设备 [{deviceName}] 重连成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.Warn($"[{_scopeName}] 设备 [{deviceName}] 重连失败,将在下次检查时重试");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
UpdateDeviceInfoState(deviceName, false);
|
||||
LoggerHelper.Warn($"[{_scopeName}] 设备 [{deviceName}] 重连超时(10s),将在下次检查时重试");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateDeviceInfoState(deviceName, false);
|
||||
LoggerHelper.Error($"[{_scopeName}] 设备 [{deviceName}] 重连异常:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 UI 线程更新 SystemConfig.DeviceList 中对应设备的 IsConnected 状态(驱动 UI 刷新)。
|
||||
/// <para>Timer 回调运行在线程池线程上,必须切回 UI 线程才能触发 PropertyChanged。</para>
|
||||
/// </summary>
|
||||
private void UpdateDeviceInfoState(string deviceName, bool isConnected)
|
||||
{
|
||||
var info = _systemConfig?.DeviceList?
|
||||
.FirstOrDefault(d => d != null &&
|
||||
string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||
if (info == null) return;
|
||||
|
||||
var dispatcher = Application.Current?.Dispatcher;
|
||||
if (dispatcher == null || dispatcher.CheckAccess())
|
||||
{
|
||||
// 已在 UI 线程(或无 Dispatcher),直接赋值
|
||||
info.IsConnected = isConnected;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 切回 UI 线程赋值,避免跨线程 PropertyChanged 异常
|
||||
dispatcher.BeginInvoke(() => info.IsConnected = isConnected);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ using System.Reflection;
|
||||
using TSMasterCAN;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
using TSMasterCAN;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
@@ -25,17 +24,20 @@ namespace UIShare.GlobalVariable
|
||||
{
|
||||
private object _lockObj = new object();
|
||||
public SystemConfig _systemConfig { get; set; }
|
||||
public CANMonitoringService _CANMonitoringService { get; set; }
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
private readonly string _scopeName;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
/// <summary>设备健康监控器:独立心跳检测 + 自动重连,与监控采样互不冲突</summary>
|
||||
private DeviceHealthMonitor? _healthMonitor;
|
||||
|
||||
/// <summary>按 DeviceName 索引的设备字典,便于业务层按名取实例。</summary>
|
||||
public IDictionary<string, IBaseInterface> DeviceMap { get; private set; }
|
||||
= new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
|
||||
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
|
||||
public CAN CANFD { get; set; }
|
||||
public IOBoardGroup IOGroup { get; set; }
|
||||
|
||||
public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo, IEventAggregator eventAggregator)
|
||||
@@ -43,11 +45,17 @@ namespace UIShare.GlobalVariable
|
||||
_systemConfig = systemConfig;
|
||||
_globalInfo = globalInfo;
|
||||
_eventAggregator = eventAggregator;
|
||||
_CANMonitoringService=new CANMonitoringService(_systemConfig);
|
||||
// 用 SystemConfig.Title 作为作用域唯一标识,无需反查 ConfigDic
|
||||
_scopeName = _systemConfig.Title;
|
||||
InitDevices();
|
||||
InitCAN();
|
||||
}
|
||||
|
||||
public void InitCAN()
|
||||
{
|
||||
var re1 = _CANMonitoringService.Init();
|
||||
}
|
||||
/// <summary>
|
||||
/// 根据设备配置提取唯一的硬件指纹字符串。
|
||||
/// <para>Tcp → "Tcp:IP:Port";Serial → "Serial:PortName";无法识别则返回空字符串。</para>
|
||||
@@ -98,55 +106,6 @@ namespace UIShare.GlobalVariable
|
||||
{
|
||||
if (config == null || !config.IsEnabled) continue;
|
||||
|
||||
// CAN 设备:ZLGCANFD 不实现 IBaseInterface,通过 SystemConfig.CANFD 单独管理,
|
||||
// 按指纹从全局 CanPool 创建/复用实例,并注册作用域引用计数。
|
||||
if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fingerprint = ExtractHardwareFingerprint(config);
|
||||
if (string.IsNullOrEmpty(fingerprint))
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{config.DeviceName}] 无法提取硬件指纹(连接方式={config.ConnectionType}),已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (config.CANConfig == null)
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{config.DeviceName}] 缺少 CAN 连接参数,已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 按指纹全局唯一创建同星 CAN 实例(maxChannels 默认 4,对应 USBCANFD-400U)
|
||||
// 波特率与终端电阻从 CANConfigVM 传入,初始化并启动通道 时直接使用
|
||||
var canLazy = _globalInfo.CanPool.GetOrAdd(fingerprint, key => new Lazy<CAN>(() =>
|
||||
new CAN(config.CANConfig.DeviceType, config.CANConfig.DeviceIndex, 4,
|
||||
config.CANConfig.ABitBaud, config.CANConfig.DBitBaud, config.CANConfig.EnableTerminalResistance)));
|
||||
|
||||
_systemConfig.CANFD = canLazy.Value;
|
||||
CANFD = canLazy.Value;
|
||||
|
||||
// 注册作用域引用计数
|
||||
if (!string.IsNullOrEmpty(_scopeName))
|
||||
{
|
||||
var scopeList = _globalInfo.DeviceAndScopeDic.GetOrAdd(fingerprint,
|
||||
_ => new Lazy<List<string>>(() => new List<string>())).Value;
|
||||
lock (scopeList)
|
||||
{
|
||||
if (!scopeList.Contains(_scopeName))
|
||||
scopeList.Add(_scopeName);
|
||||
}
|
||||
}
|
||||
|
||||
LoggerHelper.Info($"已加载 CAN 设备 [{config.DeviceName}] 指纹={fingerprint}(通过 SystemConfig.CANFD 管理)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"CAN 设备 [{config.DeviceName}] 实例化失败:{ex.Message}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DeviceType) ||
|
||||
!_deviceTypeMap.TryGetValue(config.DeviceType, out var deviceType))
|
||||
{
|
||||
@@ -253,6 +212,30 @@ namespace UIShare.GlobalVariable
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// 所有设备连接完成后,启动健康监控(心跳重连)
|
||||
StartHealthMonitor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动设备健康监控器:周期性检查设备连接状态,断连自动重连。
|
||||
/// 与 HardwareDataBroadcaster 的监控采样互不干扰。
|
||||
/// </summary>
|
||||
private void StartHealthMonitor()
|
||||
{
|
||||
if (_healthMonitor != null || DeviceMap.Count == 0) return;
|
||||
|
||||
_healthMonitor = new DeviceHealthMonitor(DeviceMap, _systemConfig, _scopeName);
|
||||
_healthMonitor.Start();
|
||||
LoggerHelper.Info($"[{_scopeName}] 心跳重连机制已激活");
|
||||
}
|
||||
|
||||
/// <summary>停止设备健康监控器</summary>
|
||||
private void StopHealthMonitor()
|
||||
{
|
||||
_healthMonitor?.Stop();
|
||||
_healthMonitor?.Dispose();
|
||||
_healthMonitor = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -302,7 +285,7 @@ namespace UIShare.GlobalVariable
|
||||
// CAN 设备:直接关闭 CAN 卡
|
||||
if (info != null && string.Equals(info.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await CloseCanAsync(info);
|
||||
CloseCan();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -321,11 +304,14 @@ namespace UIShare.GlobalVariable
|
||||
/// </summary>
|
||||
public async Task CloseAllDevicesAsync()
|
||||
{
|
||||
// 先停止健康监控,避免重连定时器与关闭操作冲突
|
||||
StopHealthMonitor();
|
||||
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
lock (_lockObj)
|
||||
{
|
||||
if (DeviceMap.Count == 0 && (CANFD == null)) return;
|
||||
if (DeviceMap.Count == 0 ) return;
|
||||
|
||||
foreach (var kvp in DeviceMap)
|
||||
{
|
||||
@@ -338,17 +324,7 @@ namespace UIShare.GlobalVariable
|
||||
}
|
||||
}
|
||||
|
||||
// CAN 设备:直接关闭 CAN 卡
|
||||
if (CANFD != null)
|
||||
{
|
||||
var canInfo = _systemConfig?.DeviceList?
|
||||
.FirstOrDefault(d => d != null && string.Equals(d.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase));
|
||||
if (canInfo != null)
|
||||
{
|
||||
tasks.Add(CloseCanAsync(canInfo));
|
||||
}
|
||||
}
|
||||
|
||||
CAN.DisConnect();
|
||||
await Task.WhenAll(tasks);
|
||||
LoggerHelper.Info("所有设备已执行关闭操作。");
|
||||
}
|
||||
@@ -456,26 +432,11 @@ namespace UIShare.GlobalVariable
|
||||
/// </summary>
|
||||
private async Task<bool> ConnectCanAsync(DeviceInfoVM info)
|
||||
{
|
||||
string name = info.DeviceName ?? "CAN";
|
||||
|
||||
try
|
||||
{
|
||||
if (CANFD == null)
|
||||
{
|
||||
LoggerHelper.Warn($"CAN 设备 [{name}] 尚未实例化,无法连接。");
|
||||
info.IsConnected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.IsConnected)
|
||||
{
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 已连接,跳过。");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = await Task.Run(() =>
|
||||
{
|
||||
if (!CANFD.打开设备()) return false;
|
||||
// 自动加载 DBC 文件
|
||||
if (_systemConfig?.DBCAutoLoadList != null)
|
||||
{
|
||||
@@ -495,8 +456,7 @@ namespace UIShare.GlobalVariable
|
||||
LoggerHelper.Warn($"CAN 通道 {item.DBCChannel} 自动加载 DBC 失败:文件不存在 [{item.DBCFilePath}]");
|
||||
continue;
|
||||
}
|
||||
CANFD.初始化并启动通道((uint)item.DBCChannel);
|
||||
bool loadOk = CANFD.加载通道DBC文件((uint)item.DBCChannel, item.DBCFilePath);
|
||||
bool loadOk = CAN.LoadDBC(item.DBCFilePath,new int[] { item.DBCChannel },out _ )==0;
|
||||
if (loadOk)
|
||||
{
|
||||
LoggerHelper.Info($"CAN 通道 {item.DBCChannel} 已自动加载 DBC:{item.DBCFilePath}");
|
||||
@@ -518,16 +478,16 @@ namespace UIShare.GlobalVariable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (CAN.Connect() != 0) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
info.IsConnected = ok;
|
||||
|
||||
if (ok)
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 连接成功,已初始化 {CANFD.DBCParser.MaxChannels} 个通道。");
|
||||
LoggerHelper.Info($"CAN 设备 连接成功");
|
||||
else
|
||||
LoggerHelper.Warn($"CAN 设备 [{name}] 连接失败。");
|
||||
LoggerHelper.Warn($"CAN 设备连接失败。");
|
||||
|
||||
return ok;
|
||||
}
|
||||
@@ -535,7 +495,7 @@ namespace UIShare.GlobalVariable
|
||||
{
|
||||
info.IsConnected = false;
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"CAN 设备 [{name}] 连接异常:{inner}");
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"CAN 设备 连接异常:{inner}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -543,36 +503,9 @@ namespace UIShare.GlobalVariable
|
||||
/// <summary>
|
||||
/// 关闭 CAN 卡
|
||||
/// </summary>
|
||||
private async Task CloseCanAsync(DeviceInfoVM info)
|
||||
private void CloseCan()
|
||||
{
|
||||
string name = info.DeviceName ?? "CAN";
|
||||
|
||||
try
|
||||
{
|
||||
if (CANFD == null)
|
||||
{
|
||||
info.IsConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.IsConnected)
|
||||
{
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 本就处于断开状态。");
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Run(() => CANFD.关闭CAN卡设备());
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 已成功关闭连接。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.Error($"CAN 设备 [{name}] 关闭连接时出现异常: {inner}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
info.IsConnected = false;
|
||||
}
|
||||
CAN.DisConnect();
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, Type> BuildDeviceTypeMap()
|
||||
@@ -620,6 +553,12 @@ namespace UIShare.GlobalVariable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 停止健康监控
|
||||
StopHealthMonitor();
|
||||
|
||||
// 停止 CAN 信号监测服务
|
||||
_CANMonitoringService?.Stop();
|
||||
|
||||
if (string.IsNullOrEmpty(_scopeName)) return;
|
||||
|
||||
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
|
||||
@@ -655,16 +594,6 @@ namespace UIShare.GlobalVariable
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从 CanPool 取出并销毁
|
||||
if (_globalInfo.CanPool.TryRemove(fingerprint, out var canLazy))
|
||||
{
|
||||
if (canLazy.IsValueCreated)
|
||||
{
|
||||
try { canLazy.Value.Dispose(); }
|
||||
catch { /* 销毁时忽略异常 */ }
|
||||
LoggerHelper.Info($"指纹 [{fingerprint}] 无作用域引用,已销毁 CAN 设备实例。");
|
||||
}
|
||||
}
|
||||
|
||||
// 同步清除 DeviceAndScopeDic 中的空条目
|
||||
_globalInfo.DeviceAndScopeDic.TryRemove(fingerprint, out _);
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace UIShare.GlobalVariable
|
||||
public class GlobalInfo:BindableBase
|
||||
{
|
||||
public event EventHandler? ScopeChanged;
|
||||
public Dictionary<string,ScopedContext> ContextDic { get; set; }
|
||||
public Dictionary<string,StepRunning> StepRunningDic { get; set; }
|
||||
public Dictionary<string, SystemConfig> ConfigDic { get; set; }
|
||||
public Dictionary<string, IScopedProvider> ScopeDic { get; set; }
|
||||
public ConcurrentDictionary<string,ScopedContext> ContextDic { get; set; }
|
||||
public ConcurrentDictionary<string,StepRunning> StepRunningDic { get; set; }
|
||||
public ConcurrentDictionary<string, SystemConfig> ConfigDic { get; set; }
|
||||
public ConcurrentDictionary<string, IScopedProvider> ScopeDic { get; set; }
|
||||
|
||||
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
|
||||
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Logger;
|
||||
using Model.Models;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
@@ -41,6 +43,18 @@ namespace UIShare.GlobalVariable
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>是否已完成过至少一次 Discover(幂等标记,避免多作用域重复调用时 Clear + 重扫)</summary>
|
||||
private bool _discovered;
|
||||
|
||||
/// <summary>连续失败次数达到此阈值后,暂停该通道采样并发布报警</summary>
|
||||
private const int FailureThreshold = 5;
|
||||
|
||||
/// <summary>每通道连续失败计数(key = fingerprint + "|" + methodName)</summary>
|
||||
private readonly ConcurrentDictionary<string, int> _failureCounts = new();
|
||||
|
||||
/// <summary>已因连续失败而被暂停的通道(key 同上)</summary>
|
||||
private readonly ConcurrentDictionary<string, bool> _suspendedChannels = new();
|
||||
|
||||
/// <summary>采样间隔(默认 1000ms)</summary>
|
||||
public TimeSpan SampleInterval
|
||||
{
|
||||
@@ -89,9 +103,12 @@ namespace UIShare.GlobalVariable
|
||||
/// <summary>
|
||||
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
||||
/// 反查可监测方法并编译为委托。每个指纹只注册一次,反射只执行一次。
|
||||
/// <para>幂等:首次调用后再次调用不会 Clear 重扫,避免多作用域重复 Discover 导致短暂采样中断。</para>
|
||||
/// </summary>
|
||||
public void Discover()
|
||||
{
|
||||
if (_discovered) return;
|
||||
_discovered = true;
|
||||
_registeredMethods.Clear();
|
||||
|
||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||
@@ -157,6 +174,11 @@ namespace UIShare.GlobalVariable
|
||||
|
||||
foreach (var entry in _registeredMethods)
|
||||
{
|
||||
string channelKey = entry.Fingerprint + "|" + entry.MethodName;
|
||||
|
||||
// 已暂停的通道跳过采样
|
||||
if (_suspendedChannels.ContainsKey(channelKey)) continue;
|
||||
|
||||
// fire-and-forget:每个通道独立采样,完成后自行广播
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
@@ -167,6 +189,9 @@ namespace UIShare.GlobalVariable
|
||||
|
||||
if (!double.TryParse(raw, out double value)) return;
|
||||
|
||||
// 采样成功,重置失败计数
|
||||
_failureCounts.TryRemove(channelKey, out _);
|
||||
|
||||
// 向所有引用该物理设备的作用域分别广播
|
||||
var scopes = GetScopesForFingerprint(entry.Fingerprint);
|
||||
foreach (var scope in scopes)
|
||||
@@ -188,9 +213,24 @@ namespace UIShare.GlobalVariable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 单个通道故障不干扰其他通道
|
||||
int count = _failureCounts.AddOrUpdate(channelKey, 1, (_, c) => c + 1);
|
||||
LoggerHelper.Warn($"采样通道 [{entry.Fingerprint}/{entry.MethodName}] 第 {count} 次失败: {ex.Message}");
|
||||
|
||||
if (count >= FailureThreshold)
|
||||
{
|
||||
_suspendedChannels.TryAdd(channelKey, true);
|
||||
LoggerHelper.Error($"采样通道 [{entry.Fingerprint}/{entry.MethodName}] 连续失败 {count} 次,已暂停采样");
|
||||
|
||||
// 向所有引用该设备的作用域发布报警
|
||||
var scopes = GetScopesForFingerprint(entry.Fingerprint);
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
_eventAggregator.GetEvent<AlarmEvent>().Publish(
|
||||
(scope, entry.Fingerprint, $"通道 {entry.MethodName} 连续失败 {count} 次,已暂停"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
@@ -209,6 +249,8 @@ namespace UIShare.GlobalVariable
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_registeredMethods.Clear();
|
||||
_failureCounts.Clear();
|
||||
_suspendedChannels.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using Prism.Mvvm;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个 Tab(主程序 / 错误程序)的子程序导航状态。
|
||||
/// 主程序和错误程序各自持有一个独立实例,互不干扰。
|
||||
/// </summary>
|
||||
public class ProgramNavigationState : BindableBase
|
||||
{
|
||||
/// <summary>是否为错误程序 Tab</summary>
|
||||
public bool IsErrorTab { get; set; }
|
||||
|
||||
public Stack<ProgramVM> NavigationStack { get; } = new();
|
||||
|
||||
private ProgramVM _currentProgram;
|
||||
public ProgramVM CurrentProgram
|
||||
{
|
||||
get => _currentProgram;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _currentProgram, value))
|
||||
RaisePropertyChanged(nameof(DisplaySteps));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataGrid 实际绑定的步骤集合。
|
||||
/// 根程序 + 错误 Tab → ErrorStepCollection;其他情况 → StepCollection。
|
||||
/// </summary>
|
||||
public ObservableCollection<StepVM> DisplaySteps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CurrentProgram == null)
|
||||
return new ObservableCollection<StepVM>();
|
||||
// 在根程序且是错误 Tab → 显示错误步骤集合
|
||||
if (NavigationStack.Count == 0 && IsErrorTab)
|
||||
return CurrentProgram.ErrorStepCollection;
|
||||
// 其他情况(主 Tab 或已进入子程序)→ 显示正常步骤集合
|
||||
return CurrentProgram.StepCollection;
|
||||
}
|
||||
}
|
||||
|
||||
private string _breadcrumbPath = "主程序";
|
||||
public string BreadcrumbPath
|
||||
{
|
||||
get => _breadcrumbPath;
|
||||
set => SetProperty(ref _breadcrumbPath, value);
|
||||
}
|
||||
|
||||
private bool _canGoBack;
|
||||
public bool CanGoBack
|
||||
{
|
||||
get => _canGoBack;
|
||||
set => SetProperty(ref _canGoBack, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用根程序初始化导航状态
|
||||
/// </summary>
|
||||
public void Initialize(ProgramVM rootProgram, string label)
|
||||
{
|
||||
NavigationStack.Clear();
|
||||
CurrentProgram = rootProgram;
|
||||
BreadcrumbPath = label;
|
||||
CanGoBack = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空导航栈,回到根程序
|
||||
/// </summary>
|
||||
public void Reset(ProgramVM rootProgram, string label)
|
||||
{
|
||||
NavigationStack.Clear();
|
||||
CurrentProgram = rootProgram;
|
||||
BreadcrumbPath = label;
|
||||
CanGoBack = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据栈状态刷新 CanGoBack
|
||||
/// </summary>
|
||||
public void UpdateCanGoBack() => CanGoBack = NavigationStack.Count > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using DeviceCommand.Base;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -14,9 +15,35 @@ using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public class ScopedContext
|
||||
public class ScopedContext : BindableBase
|
||||
{
|
||||
private static readonly Random _randomSeed = new Random();
|
||||
|
||||
#region 子程序导航(主程序 / 错误程序各自独立)
|
||||
|
||||
/// <summary>主程序 Tab 的导航状态</summary>
|
||||
public ProgramNavigationState MainNav { get; } = new();
|
||||
|
||||
/// <summary>错误程序 Tab 的导航状态</summary>
|
||||
public ProgramNavigationState ErrorNav { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前选中的 Tab 返回对应的导航状态
|
||||
/// </summary>
|
||||
public ProgramNavigationState ActiveNav =>
|
||||
SelectedStepList == "错误程序" ? ErrorNav : MainNav;
|
||||
|
||||
/// <summary>
|
||||
/// 重置两个 Tab 的导航栈,回到根程序
|
||||
/// </summary>
|
||||
public void ResetNavigation()
|
||||
{
|
||||
MainNav.Reset(Program, "主程序");
|
||||
ErrorNav.Reset(Program, "错误程序");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ProgramVM Program { get; set; } = new();
|
||||
public String SelectedStepList { get; set; } = "主程序";
|
||||
public string CurrentFilePath { get; set; }
|
||||
@@ -45,6 +72,7 @@ namespace UIShare.GlobalVariable
|
||||
public int DebugRandomId { get; private set; }
|
||||
public ScopedContext()
|
||||
{
|
||||
ErrorNav.IsErrorTab = true;
|
||||
lock (_randomSeed)
|
||||
{
|
||||
// 每次诞生一个新上下文,就在 10000 到 99999 之间随机摇一个数
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using UIShare.PubEvent;
|
||||
using Common.Tools;
|
||||
using Logger;
|
||||
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Model.Entity;
|
||||
using Service.Interface;
|
||||
@@ -13,6 +14,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using UIShare.GlobalVariable;
|
||||
using static UIShare.UIViewModel.ParameterVM;
|
||||
@@ -29,6 +31,7 @@ namespace UIShare
|
||||
private IContainerProvider containerProvider;
|
||||
private IEventAggregator _eventAggregator;
|
||||
private ITestReportService _testReportService;
|
||||
private ITestCheckRecordService _testCheckRecordService;
|
||||
|
||||
private readonly Dictionary<Guid, ParameterVM> tmpParameters = [];
|
||||
|
||||
@@ -38,6 +41,9 @@ namespace UIShare
|
||||
|
||||
private readonly Stack<LoopContext> loopStack = new();
|
||||
|
||||
/// <summary>测试项上下文栈:进入 IsTestItem 子程序时压栈,栈非空期间每次 OKExpression 判断都归属栈顶测试项</summary>
|
||||
private readonly Stack<TestItemContext> testItemStack = new();
|
||||
|
||||
public CancellationTokenSource stepCTS = new();
|
||||
public CancellationTokenSource errorStepCTS = new();
|
||||
private bool SubSingleStep = false;
|
||||
@@ -46,13 +52,14 @@ namespace UIShare
|
||||
private volatile bool _disposed = false;
|
||||
|
||||
public Guid TestRoundID;
|
||||
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService)
|
||||
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService, ITestCheckRecordService testCheckRecordService)
|
||||
{
|
||||
_scopedContext = ScopedContext;
|
||||
_systemConfig = systemConfig;
|
||||
_eventAggregator = eventAggregator;
|
||||
_deviceManager= deviceManager;
|
||||
_testReportService = testReportService;
|
||||
_testCheckRecordService = testCheckRecordService;
|
||||
//_devices = containerProvider.Resolve<Devices>();
|
||||
}
|
||||
public async Task<bool> ExecuteErrorSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
||||
@@ -68,11 +75,11 @@ namespace UIShare
|
||||
tmpParameters.Clear();
|
||||
TestRoundID = Guid.NewGuid();
|
||||
}
|
||||
int initialLoopStackCount = loopStack.Count;
|
||||
foreach (var item in program.Parameters)
|
||||
{
|
||||
tmpParameters.TryAdd(item.ID, item);
|
||||
}
|
||||
|
||||
while (index < program.ErrorStepCollection.Count)
|
||||
{
|
||||
if (_disposed || cancellationToken.IsCancellationRequested)
|
||||
@@ -181,7 +188,23 @@ namespace UIShare
|
||||
_scopedContext.SingleStep = false;
|
||||
}
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||
// 发布进入子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Enter,
|
||||
SubProgram = step.SubProgram,
|
||||
StepName = step.Name,
|
||||
IsErrorProgram = true
|
||||
});
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
// 发布退出子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Exit,
|
||||
IsErrorProgram = true
|
||||
});
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (SubSingleStep)
|
||||
{
|
||||
@@ -223,7 +246,12 @@ namespace UIShare
|
||||
await SaveStepRecordAsync(step, depth, true);
|
||||
}
|
||||
}
|
||||
bool finalResult = loopStack.Count == initialLoopStackCount && stepSuccess;
|
||||
|
||||
if (depth > 0) // 子程序
|
||||
{
|
||||
return finalResult;
|
||||
}
|
||||
return loopStack.Count == 0 && stepSuccess;
|
||||
}
|
||||
public async Task<bool> ExecuteSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
||||
@@ -237,8 +265,10 @@ namespace UIShare
|
||||
loopStopwatchStack.Clear();
|
||||
ResetAllStepStatus(program.StepCollection);
|
||||
tmpParameters.Clear();
|
||||
testItemStack.Clear();
|
||||
TestRoundID = Guid.NewGuid();
|
||||
}
|
||||
int initialLoopStackCount = loopStack.Count;
|
||||
foreach (var item in program.Parameters)
|
||||
{
|
||||
tmpParameters.TryAdd(item.ID, item);
|
||||
@@ -356,8 +386,32 @@ namespace UIShare
|
||||
_scopedContext.SingleStep = false;
|
||||
}
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||
// 发布进入子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Enter,
|
||||
SubProgram = step.SubProgram,
|
||||
StepName = step.Name
|
||||
});
|
||||
bool isTestItemStep = step.IsTestItem;
|
||||
if (isTestItemStep)
|
||||
{
|
||||
testItemStack.Push(new TestItemContext { Name = step.Name ?? "未命名测试项" });
|
||||
}
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
// 先评估本步骤自身(含自身 OKExpression 判断,归属本测试项),再写测试项汇总并弹栈
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (isTestItemStep)
|
||||
{
|
||||
await FinalizeTestItemAsync(depth);
|
||||
}
|
||||
// 发布退出子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Exit
|
||||
});
|
||||
if (SubSingleStep)
|
||||
{
|
||||
SubSingleStep = false;
|
||||
@@ -403,9 +457,15 @@ namespace UIShare
|
||||
_eventAggregator.GetEvent<RunSingalCompletedEvent>().Publish("Play");
|
||||
}
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
// 仅检查本层执行期间压入的循环是否全部弹出,不关心父程序遗留的循环上下文
|
||||
}
|
||||
}
|
||||
bool finalResult = loopStack.Count == initialLoopStackCount && stepSuccess;
|
||||
|
||||
if (depth > 0) // 子程序
|
||||
{
|
||||
return finalResult;
|
||||
}
|
||||
return loopStack.Count == 0 && stepSuccess;
|
||||
}
|
||||
|
||||
@@ -578,11 +638,7 @@ namespace UIShare
|
||||
{
|
||||
try
|
||||
{
|
||||
if(targetType.Name== "ZLGCANFD")
|
||||
{
|
||||
instance = _deviceManager.CANFD;
|
||||
}
|
||||
else if (targetType.Name == "IOBoardGroup")
|
||||
if (targetType.Name == "IOBoardGroup")
|
||||
{
|
||||
instance = _deviceManager.IOGroup;
|
||||
}
|
||||
@@ -776,12 +832,46 @@ namespace UIShare
|
||||
paraDic.TryAdd(item.Name, item.Value!);
|
||||
}
|
||||
}
|
||||
bool re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
||||
bool re;
|
||||
try
|
||||
{
|
||||
re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 表达式执行错误也视为 NG,并记录到测试项判断明细
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] OKExpression 执行异常: {ex.Message}", depth: depth);
|
||||
re = false;
|
||||
}
|
||||
step.Result = re ? 1 : 2;
|
||||
if (step.Result == 2)
|
||||
{
|
||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth);
|
||||
}
|
||||
// 测试项范围内:记录本次判断(重复判断逐次记录)
|
||||
if (testItemStack.Count > 0)
|
||||
{
|
||||
var ctx = testItemStack.Peek();
|
||||
bool isSelfCheck = ctx.Name == (step.Name ?? "未命名测试项") && step.SubProgram != null;
|
||||
if (!re) ctx.HasFailure = true;
|
||||
if (!isSelfCheck)
|
||||
{
|
||||
SaveCheckRecordAsync(new TestCheckRecordEntity
|
||||
{
|
||||
TestRoundId = TestRoundID,
|
||||
Scope = _systemConfig.Title,
|
||||
FileName = _systemConfig.CurrentACPFile ?? "",
|
||||
TestItemName = ctx.Name,
|
||||
StepName = step.Name ?? "",
|
||||
Depth = depth,
|
||||
OKExpression = step.OKExpression,
|
||||
Pass = re,
|
||||
Values = ExtractExpressionValues(step.OKExpression, paraDic),
|
||||
IsSummary = false,
|
||||
CreateTime = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -792,6 +882,90 @@ namespace UIShare
|
||||
}
|
||||
step.Result = 2;
|
||||
}
|
||||
|
||||
// 测试项范围内的步骤执行错误/表达式 NG 均计入当前测试项汇总(自身步骤的错误已在压栈期间计入)
|
||||
if (step.Result == 2 && testItemStack.Count > 0)
|
||||
{
|
||||
testItemStack.Peek().HasFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试项执行结束:写入汇总记录(PASS/NG)并弹栈。
|
||||
/// 测试项内无任何判断时,结果由范围内步骤执行成败决定。
|
||||
/// </summary>
|
||||
private async Task FinalizeTestItemAsync(int depth)
|
||||
{
|
||||
if (testItemStack.Count == 0) return;
|
||||
var ctx = testItemStack.Pop();
|
||||
SaveCheckRecordAsync(new TestCheckRecordEntity
|
||||
{
|
||||
TestRoundId = TestRoundID,
|
||||
Scope = _systemConfig.Title,
|
||||
FileName = _systemConfig.CurrentACPFile ?? "",
|
||||
TestItemName = ctx.Name,
|
||||
StepName = ctx.Name,
|
||||
Depth = depth,
|
||||
OKExpression = null,
|
||||
Pass = !ctx.HasFailure,
|
||||
Values = null,
|
||||
IsSummary = true,
|
||||
CreateTime = DateTime.Now
|
||||
});
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存测试项判断记录(不阻塞执行主流程)
|
||||
/// </summary>
|
||||
private void SaveCheckRecordAsync(TestCheckRecordEntity entity)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _testCheckRecordService.InsertAsync(entity);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {result.Msg}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从表达式中提取实际出现的变量并取其当前值,拼接为 "变量=值; " 格式(长名优先,避免子串误匹配)
|
||||
/// </summary>
|
||||
private static string? ExtractExpressionValues(string expression, Dictionary<string, object> paraDic)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var matchedSpans = new List<(int Start, int End)>();
|
||||
foreach (var name in paraDic.Keys.OrderByDescending(k => k.Length))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
foreach (Match m in Regex.Matches(expression, $@"\b{Regex.Escape(name)}\b"))
|
||||
{
|
||||
bool overlaps = matchedSpans.Any(s => m.Index < s.End && m.Index + m.Length > s.Start);
|
||||
if (!overlaps)
|
||||
{
|
||||
matchedSpans.Add((m.Index, m.Index + m.Length));
|
||||
sb.Append($"{name}={paraDic[name]}; ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.Length > 0 ? sb.ToString() : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -825,6 +999,7 @@ namespace UIShare
|
||||
tmpParameters.Clear();
|
||||
loopStack.Clear();
|
||||
loopStopwatchStack.Clear();
|
||||
testItemStack.Clear();
|
||||
stepStopwatch.Stop();
|
||||
}
|
||||
|
||||
@@ -840,6 +1015,13 @@ namespace UIShare
|
||||
public StepVM? LoopStartStep { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>测试项执行上下文:记录测试项名称与范围内是否出现过失败</summary>
|
||||
private class TestItemContext
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool HasFailure { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -20,12 +20,22 @@ namespace UIShare.GlobalVariable
|
||||
{
|
||||
if (globalInfo?.ConfigDic == null) return "";
|
||||
if (!globalInfo.ConfigDic.TryGetValue(scope, out var systemConfig)) return "";
|
||||
if (systemConfig.ValueLimitList == null) return "";
|
||||
return CheckAlarm(fingerprint, methodName, value, systemConfig);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接根据 SystemConfig 的 ValueLimitList 检查当前值是否超限。
|
||||
/// 适用于不持有 GlobalInfo 引用的场景(如 CANSignalBroadcaster)。
|
||||
/// </summary>
|
||||
public static string CheckAlarm(string fingerprint, string methodName, double value, SystemConfig systemConfig)
|
||||
{
|
||||
if (systemConfig?.ValueLimitList == null) return "";
|
||||
|
||||
var limit = systemConfig.ValueLimitList.FirstOrDefault(x =>
|
||||
x.Fingerprint == fingerprint && x.MethodName == methodName);
|
||||
if (limit == null) return "";
|
||||
if (value > limit.UpperExtreme)
|
||||
|
||||
if (value > limit.UpperExtreme)
|
||||
{
|
||||
limit.IsAlarm = true;
|
||||
limit.AlarmSatus = AlarmStatus.超上极限;
|
||||
|
||||
@@ -9,4 +9,22 @@ namespace UIShare.PubEvent
|
||||
public class OverlayEvent : PubSubEvent<bool>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 作用域感知的灰度遮罩事件:仅台架名称与 Scope 匹配的台架视图显示/隐藏加载遮罩,
|
||||
/// 避免全局 OverlayEvent 导致所有台架及主窗口同时变灰。
|
||||
/// </summary>
|
||||
public class ScopeOverlayEvent : PubSubEvent<ScopeOverlayArgs>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>作用域遮罩事件参数。</summary>
|
||||
public class ScopeOverlayArgs
|
||||
{
|
||||
/// <summary>台架名称(与 SystemConfig.Title / TestStatus 一致)。</summary>
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>true 显示遮罩,false 隐藏遮罩。</summary>
|
||||
public bool Show { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using Prism.Events;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 子程序导航事件:运行时进入/退出子程序时发布,
|
||||
/// StepsManagerViewModel 订阅后自动切换显示。
|
||||
/// </summary>
|
||||
public class SubProgramNavigateEvent : PubSubEvent<SubProgramNavigatePayload>
|
||||
{
|
||||
}
|
||||
|
||||
public class SubProgramNavigatePayload
|
||||
{
|
||||
/// <summary>目标台架的作用域标识</summary>
|
||||
public string Scope { get; set; } = "";
|
||||
|
||||
/// <summary>Enter = 进入子程序,Exit = 退出回到上一级</summary>
|
||||
public NavigateAction Action { get; set; }
|
||||
|
||||
/// <summary>进入时:子程序的 ProgramVM;退出时可为 null</summary>
|
||||
public ProgramVM? SubProgram { get; set; }
|
||||
|
||||
/// <summary>子程序步骤的名称(用于面包屑显示)</summary>
|
||||
public string? StepName { get; set; }
|
||||
|
||||
/// <summary>是否为错误程序 Tab 的导航(默认 false = 主程序 Tab)</summary>
|
||||
public bool IsErrorProgram { get; set; }
|
||||
}
|
||||
|
||||
public enum NavigateAction
|
||||
{
|
||||
Enter,
|
||||
Exit
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
@@ -26,4 +26,9 @@
|
||||
<ProjectReference Include="..\Service\Service.csproj" />
|
||||
<ProjectReference Include="..\TSMasterCAN\TSMasterCAN.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Interop.TSMasterAPI">
|
||||
<HintPath>..\TSMasterCAN\Interop.TSMasterAPI.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace UIShare.UIViewModel
|
||||
public class InstructionNodeVM : BindableBase
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
@@ -32,5 +32,12 @@ namespace UIShare.UIViewModel
|
||||
get => _tag;
|
||||
set => SetProperty(ref _tag, value);
|
||||
}
|
||||
|
||||
private string? _tooltip;
|
||||
public string? Tooltip
|
||||
{
|
||||
get => _tooltip;
|
||||
set => SetProperty(ref _tooltip, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace UIShare.UIViewModel
|
||||
NGGotoStepID = source.NGGotoStepID;
|
||||
Description = source.Description;
|
||||
IsUsed = source.IsUsed;
|
||||
IsTestItem = source.IsTestItem;
|
||||
|
||||
if (source.Method != null)
|
||||
{
|
||||
@@ -55,6 +56,15 @@ namespace UIShare.UIViewModel
|
||||
set => SetProperty(ref _isUsed, value);
|
||||
}
|
||||
|
||||
private bool _isTestItem = false;
|
||||
|
||||
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
|
||||
public bool IsTestItem
|
||||
{
|
||||
get => _isTestItem;
|
||||
set => SetProperty(ref _isTestItem, value);
|
||||
}
|
||||
|
||||
private int _index;
|
||||
|
||||
public int Index
|
||||
|
||||
Reference in New Issue
Block a user