Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c017efdb49 | ||
|
|
81c6386e62 | ||
|
|
3275283263 |
@@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACP", "ACP\ACP.csproj", "{C
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterCAN", "TSMasterCAN\TSMasterCAN.csproj", "{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterCAN", "TSMasterCAN\TSMasterCAN.csproj", "{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerAdaptabilityModule", "PowerAdaptabilityModule\PowerAdaptabilityModule.csproj", "{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -131,6 +133,10 @@ Global
|
|||||||
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -146,5 +152,6 @@ Global
|
|||||||
{AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
{AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
{84006890-9288-44C4-9D17-FA88F3FDECFA} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
{84006890-9288-44C4-9D17-FA88F3FDECFA} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
{8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
{8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
|
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<ProjectReference Include="..\LoginModule\LoginModule.csproj" />
|
<ProjectReference Include="..\LoginModule\LoginModule.csproj" />
|
||||||
<ProjectReference Include="..\MainModule\MainModule.csproj" />
|
<ProjectReference Include="..\MainModule\MainModule.csproj" />
|
||||||
<ProjectReference Include="..\MonitorModule\MonitorModule.csproj" />
|
<ProjectReference Include="..\MonitorModule\MonitorModule.csproj" />
|
||||||
|
<ProjectReference Include="..\PowerAdaptabilityModule\PowerAdaptabilityModule.csproj" />
|
||||||
<ProjectReference Include="..\Service\Service.csproj" />
|
<ProjectReference Include="..\Service\Service.csproj" />
|
||||||
<ProjectReference Include="..\SettingModule\SettingModule.csproj" />
|
<ProjectReference Include="..\SettingModule\SettingModule.csproj" />
|
||||||
<ProjectReference Include="..\TestingModule\TestingModule.csproj" />
|
<ProjectReference Include="..\TestingModule\TestingModule.csproj" />
|
||||||
|
|||||||
@@ -902,6 +902,10 @@ namespace ACP.ViewModels
|
|||||||
if (_globalInfo.CurrentScope != "default") return;
|
if (_globalInfo.CurrentScope != "default") return;
|
||||||
_regionManager.RequestNavigate("ShellViewManager", "ExportView");
|
_regionManager.RequestNavigate("ShellViewManager", "ExportView");
|
||||||
break;
|
break;
|
||||||
|
case "电源适应性测试":
|
||||||
|
if (_globalInfo.CurrentScope != "default") return;
|
||||||
|
_regionManager.RequestNavigate("ShellViewManager", "PowerAdaptabilityView");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,11 @@
|
|||||||
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
Margin="8" />
|
Margin="8" />
|
||||||
|
<Button Content="电源适应性测试"
|
||||||
|
Command="{Binding NavigateCommand}"
|
||||||
|
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
||||||
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
|
Margin="8" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</materialDesign:DrawerHost.LeftDrawerContent>
|
</materialDesign:DrawerHost.LeftDrawerContent>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DeviceCommand.Base
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VISA SCPI 通信接口:与 <see cref="ITcp"/> 暴露一致的异步收发能力,
|
||||||
|
/// 使 SCPI 设备类可在 Tcp 与 VISA 传输之间平滑切换。
|
||||||
|
/// </summary>
|
||||||
|
public interface IVisa : IBaseInterface, IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>发送原始二进制数据。</summary>
|
||||||
|
Task SendAsync(byte[] buffer, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>发送字符串命令(SCPI 命令自带结束符)。</summary>
|
||||||
|
Task SendAsync(string str, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>读取指定长度的原始字节。</summary>
|
||||||
|
Task<byte[]> ReadAsync(int length, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>读取一行字符串,直到遇到结束符。</summary>
|
||||||
|
Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>发送命令并读取一行响应。</summary>
|
||||||
|
Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>发送查询命令并读取 IEEE 488.2 定长二进制块,自动剥离块头(#N<长度>)后只返回纯数据字节(用于截图/波形等大块数据)。</summary>
|
||||||
|
Task<byte[]> ReadAllBytesAsync(string queryCommand, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
using Model.Models;
|
||||||
|
using NationalInstruments.Visa;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DeviceCommand.Base
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VISA SCPI 通信基类:基于 NI-VISA(<c>NationalInstruments.Visa</c>)实现,
|
||||||
|
/// 对外暴露与 <see cref="Tcp"/> 完全一致的异步 API(SendAsync / ReadAsync / WriteReadAsync / ReadAllBytesAsync),
|
||||||
|
/// 使 SCPI 设备类(如 <c>TektronixMSO</c>)能在 Tcp 与 VISA 传输之间平滑切换。
|
||||||
|
/// <para>VISA 通过统一的资源字符串寻址(TCPIP0::IP::inst0::INSTR、USB0::...、GPIB0::...::INSTR 等),
|
||||||
|
/// 由 NI-VISA 运行时自动选择对应传输层;本类只面向消息型会话 <see cref="MessageBasedSession"/>。</para>
|
||||||
|
/// <para>注意:VISA 的读写为同步阻塞调用,本基类统一通过 <see cref="Task.Run(System.Action)"/> 投递到线程池,
|
||||||
|
/// 以保持与 Tcp 一致的 async/await 契约、避免阻塞 UI 线程。</para>
|
||||||
|
/// </summary>
|
||||||
|
public class Visa : IVisa, IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>VISA 资源字符串(例如 TCPIP0::192.168.20.125::inst0::INSTR)。</summary>
|
||||||
|
public string ResourceName { get; set; } = "TCPIP0::127.0.0.1::inst0::INSTR";
|
||||||
|
|
||||||
|
/// <summary>打开 VISA 会话的最长等待时间(毫秒)。</summary>
|
||||||
|
public int OpenTimeout { get; set; } = 3000;
|
||||||
|
|
||||||
|
/// <summary>读写 I/O 超时(毫秒),对应 VISA 的 TimeoutMilliseconds。</summary>
|
||||||
|
public int IOTimeout { get; set; } = 3000;
|
||||||
|
|
||||||
|
/// <summary>底层 VISA 消息型会话;为 null 表示未连接。</summary>
|
||||||
|
private MessageBasedSession? _session;
|
||||||
|
|
||||||
|
/// <summary>通信互斥锁:VISA 会话不支持并发读写,所有 I/O 串行化。</summary>
|
||||||
|
protected readonly SemaphoreSlim _commLock = new(1, 1);
|
||||||
|
|
||||||
|
public bool IsConnected => _session != null;
|
||||||
|
|
||||||
|
public Visa()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 <see cref="VisaConfig"/> 一次性配置 VISA 通信参数。
|
||||||
|
/// </summary>
|
||||||
|
public Visa(VisaConfig config) : this()
|
||||||
|
{
|
||||||
|
if (config == null) return;
|
||||||
|
ConfigureDevice(config.ResourceName, config.OpenTimeout, config.IOTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureDevice(string resourceName, int openTimeout = 3000, int ioTimeout = 3000)
|
||||||
|
{
|
||||||
|
ResourceName = resourceName;
|
||||||
|
OpenTimeout = openTimeout;
|
||||||
|
IOTimeout = ioTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<bool> ConnectAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await Task.Run(() => ResetConnection(), ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 核心内部方法:释放旧会话并按资源字符串重建 VISA 会话。
|
||||||
|
/// </summary>
|
||||||
|
private bool ResetConnection()
|
||||||
|
{
|
||||||
|
DisposeSession();
|
||||||
|
|
||||||
|
var session = CreateSession();
|
||||||
|
// 会话创建失败(资源为空/设备未连接/类型不识别):保持未连接状态返回 false,不抛异常
|
||||||
|
if (session == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// I/O 超时(读写共用)
|
||||||
|
session.TimeoutMilliseconds = IOTimeout > 0 ? IOTimeout : 3000;
|
||||||
|
// SCPI 以 LF(\n) 作为读取终止符
|
||||||
|
session.TerminationCharacter = (byte)'\n';
|
||||||
|
session.TerminationCharacterEnabled = true;
|
||||||
|
// 在写入最后一个字节时发送 END 指示
|
||||||
|
session.SendEndEnabled = true;
|
||||||
|
|
||||||
|
_session = session;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按 VISA 资源字符串前缀创建对应的具体会话实例;创建失败时返回 <c>null</c>(不抛异常)。
|
||||||
|
/// <para>NI-VISA .NET 的基类 <see cref="MessageBasedSession"/> 构造函数为 protected,无法直接实例化,
|
||||||
|
/// 必须使用其 public 构造函数的具体子类(TcpipSession/GpibSession/UsbSession/SerialSession),
|
||||||
|
/// 它们均派生自 <see cref="MessageBasedSession"/>,故可统一以基类引用持有。</para>
|
||||||
|
/// <para>资源字符串为空、类型无法识别或底层打开会话失败(如示波器未连接)时一律返回 <c>null</c>,
|
||||||
|
/// 由 <see cref="ResetConnection"/> 按“未连接”处理,避免设备缺省时抛异常中断上层初始化。</para>
|
||||||
|
/// </summary>
|
||||||
|
private MessageBasedSession? CreateSession()
|
||||||
|
{
|
||||||
|
string res = ResourceName?.Trim() ?? string.Empty;
|
||||||
|
// 资源字符串为空:视为该设备未配置/未连接,直接返回 null,不抛异常
|
||||||
|
if (res.Length == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string upper = res.ToUpperInvariant();
|
||||||
|
int openTimeout = OpenTimeout > 0 ? OpenTimeout : 3000;
|
||||||
|
var mode = Ivi.Visa.AccessModes.None;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (upper.StartsWith("TCPIP"))
|
||||||
|
return new TcpipSession(res, mode, openTimeout);
|
||||||
|
if (upper.StartsWith("GPIB"))
|
||||||
|
return new GpibSession(res, mode, openTimeout);
|
||||||
|
if (upper.StartsWith("USB"))
|
||||||
|
return new UsbSession(res, mode, openTimeout);
|
||||||
|
if (upper.StartsWith("ASRL") || upper.StartsWith("SERIAL"))
|
||||||
|
return new SerialSession(res, mode, openTimeout);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// 打开会话失败(设备未连接/资源被占用/驱动异常等):吞掉异常返回 null,交由上层按“未连接”处理
|
||||||
|
}
|
||||||
|
// 无法识别的资源类型同样返回 null,不抛异常
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Close()
|
||||||
|
{
|
||||||
|
DisposeSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>线程安全地释放当前会话并置空(幂等)。</summary>
|
||||||
|
private void DisposeSession()
|
||||||
|
{
|
||||||
|
var s = Interlocked.Exchange(ref _session, null);
|
||||||
|
try { s?.Dispose(); } catch { /* 释放阶段忽略底层异常 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>确保会话已建立,否则抛出与 Tcp 一致语义的异常。</summary>
|
||||||
|
private void EnsureConnected()
|
||||||
|
{
|
||||||
|
if (_session == null) throw new InvalidOperationException("VISA 会话未连接。");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按分隔符设置读取终止字符(取分隔符最后一个字符,兼容 "\r\n")。</summary>
|
||||||
|
private void ApplyTermination(string? delimiter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(delimiter)) return;
|
||||||
|
_session!.TerminationCharacter = (byte)delimiter[delimiter.Length - 1];
|
||||||
|
_session!.TerminationCharacterEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宽松解码设备回传的字节为字符串。
|
||||||
|
/// <para>NI-VISA 的 <c>RawIO.ReadString()</c> 采用严格解码,遇到非文本/高位字节会抛
|
||||||
|
/// <see cref="DecoderFallbackException"/>;而 SCPI 文本响应本质是 ASCII,
|
||||||
|
/// 故改用 <c>RawIO.Read()</c> 取原始字节再以 <c>Encoding.ASCII</c> 解码——
|
||||||
|
/// 非法字节被替换为 '?',绝不抛异常。</para>
|
||||||
|
/// </summary>
|
||||||
|
private static string DecodeBytes(byte[]? buffer)
|
||||||
|
=> buffer == null || buffer.Length == 0 ? string.Empty : Encoding.ASCII.GetString(buffer);
|
||||||
|
|
||||||
|
public async Task SendAsync(byte[] buffer, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
await Task.Run(() => _session!.RawIO.Write(buffer), ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendAsync(string str, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
// RawIO.Write 原样下发,不追加结束符(SCPI 命令自带 \n)
|
||||||
|
await Task.Run(() => _session!.RawIO.Write(str), ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> ReadAsync(int length, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
return await Task.Run(() => _session!.RawIO.Read(length), ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
ApplyTermination(delimiter);
|
||||||
|
// 读到终止符/END 为止(等价 Tcp 的按分隔符读取),再用宽松 ASCII 解码,避免 ReadString 严格解码抛 DecoderFallbackException
|
||||||
|
return await Task.Run(() => DecodeBytes(_session!.RawIO.Read()).Trim(), ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
ApplyTermination(delimiter);
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
_session!.RawIO.Write(command);
|
||||||
|
return DecodeBytes(_session!.RawIO.Read()).Trim();
|
||||||
|
}, ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送查询命令并读取设备回传的 IEEE 488.2 定长二进制块,自动剥离块头(<c>#N<长度></c>),只返回纯数据字节。
|
||||||
|
/// <para>示波器 <c>FILESystem:READFile</c> 回传的 PNG 截图、<c>CURVe?</c> 波形等均以该块格式封装:
|
||||||
|
/// <c>#9<9位长度><二进制数据><LF></c>。若把含 <c>#9...</c> 头的整段原始字节直接写入 .png,
|
||||||
|
/// 文件将不以 PNG 魔数(<c>\x89PNG</c>)开头而被判为损坏——故必须解析并丢弃块头,只落地纯数据。</para>
|
||||||
|
/// <para>读取期间关闭终止符,避免数据中的 0x0A 被误判为结束;并按块头声明的长度精确读取,
|
||||||
|
/// 不再依赖 END 指示,从根本上规避大块传输的 <c>IOTimeoutException</c>。</para>
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> ReadAllBytesAsync(string queryCommand, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureConnected();
|
||||||
|
return await Task.Run(() => ReadIEEEBlock(queryCommand), ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同步读取一个 IEEE 488.2 定长二进制块(<c>#N<长度><数据><终止符></c>)并返回其纯数据部分。
|
||||||
|
/// </summary>
|
||||||
|
private byte[] ReadIEEEBlock(string queryCommand)
|
||||||
|
{
|
||||||
|
bool prevTermEnabled = _session!.TerminationCharacterEnabled;
|
||||||
|
int prevTimeout = _session!.TimeoutMilliseconds;
|
||||||
|
// 二进制数据可能含 0x0A,读取全程禁用终止符,改为按长度精确读取
|
||||||
|
_session!.TerminationCharacterEnabled = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_session!.RawIO.Write(queryCommand);
|
||||||
|
|
||||||
|
// 1) 块头起始符 '#'(容忍前导空白/换行)
|
||||||
|
int first = ReadBlockStartByte();
|
||||||
|
if (first != '#')
|
||||||
|
{
|
||||||
|
// 非标准块:设备(如 Tektronix FILESystem:READFile 回传 PNG 截图)直接回裸二进制,不带 IEEE #N 块头。
|
||||||
|
// 单次 RawIO.Read() 只返回首个内部缓冲(约 1KB)即结束,会把大文件截断(表现为截图只有 1024/1025 字节)。
|
||||||
|
// 参考 ADP TCP “读到没有为止” 的策略,这里按 VISA 原生 END 指示分块循环读取,直到传输真正结束,并把已取出的首字节并回。
|
||||||
|
return PrependByte((byte)first, ReadRawUntilEnd(prevTimeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 长度位数 N('1'-'9';'0' 为不定长块)
|
||||||
|
int nDigits = ReadOneByte() - '0';
|
||||||
|
if (nDigits <= 0)
|
||||||
|
{
|
||||||
|
// 不定长块 #0<data><terminator>:启用终止符,读到换行为止
|
||||||
|
_session!.TerminationCharacter = (byte)'\n';
|
||||||
|
_session!.TerminationCharacterEnabled = true;
|
||||||
|
return _session!.RawIO.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) N 位十进制长度
|
||||||
|
byte[] lenBytes = _session!.RawIO.Read(nDigits);
|
||||||
|
string lenText = Encoding.ASCII.GetString(lenBytes).Trim();
|
||||||
|
if (!long.TryParse(lenText, out long dataLen) || dataLen < 0)
|
||||||
|
throw new InvalidOperationException($"IEEE 块头长度字段解析失败:'{lenText}'。");
|
||||||
|
if (dataLen == 0) return Array.Empty<byte>();
|
||||||
|
|
||||||
|
// 4) 精确读取 dataLen 字节(分块补足);大块传输放宽超时,避免 IOTimeoutException
|
||||||
|
_session!.TimeoutMilliseconds = Math.Max(prevTimeout, 30000);
|
||||||
|
var buffer = new byte[dataLen];
|
||||||
|
long offset = 0;
|
||||||
|
while (offset < dataLen)
|
||||||
|
{
|
||||||
|
byte[] chunk = _session!.RawIO.Read(dataLen - offset);
|
||||||
|
if (chunk == null || chunk.Length == 0) break;
|
||||||
|
Buffer.BlockCopy(chunk, 0, buffer, (int)offset, chunk.Length);
|
||||||
|
offset += chunk.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 消费块尾终止符(LF/CRLF),短超时避免无尾符时长时间阻塞,保证后续命令读取同步
|
||||||
|
_session!.TimeoutMilliseconds = 500;
|
||||||
|
_session!.TerminationCharacter = (byte)'\n';
|
||||||
|
_session!.TerminationCharacterEnabled = true;
|
||||||
|
try { _session!.RawIO.ReadString(); } catch { /* 无尾随终止符则忽略 */ }
|
||||||
|
|
||||||
|
if (offset == dataLen) return buffer;
|
||||||
|
// 实际读到的字节少于声明长度:按实到长度截断返回,避免尾部填充 0
|
||||||
|
var trimmed = new byte[offset];
|
||||||
|
Buffer.BlockCopy(buffer, 0, trimmed, 0, (int)offset);
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_session!.TimeoutMilliseconds = prevTimeout;
|
||||||
|
_session!.TerminationCharacterEnabled = prevTermEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>读取 1 个字节并返回其整数值(0-255);无数据则抛异常。</summary>
|
||||||
|
private int ReadOneByte()
|
||||||
|
{
|
||||||
|
byte[] b = _session!.RawIO.Read(1);
|
||||||
|
if (b == null || b.Length == 0)
|
||||||
|
throw new InvalidOperationException("读取 IEEE 块头时未收到任何字节。");
|
||||||
|
return b[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>跳过前导空白/换行后读取块头起始字节,返回其整数值。</summary>
|
||||||
|
private int ReadBlockStartByte()
|
||||||
|
{
|
||||||
|
int b = ReadOneByte();
|
||||||
|
for (int guard = 0; (b == '\n' || b == '\r' || b == ' ' || b == '\t') && guard < 64; guard++)
|
||||||
|
b = ReadOneByte();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>把首字节并回数组前部(用于设备直接回裸数据的退化路径)。</summary>
|
||||||
|
private static byte[] PrependByte(byte first, byte[]? rest)
|
||||||
|
{
|
||||||
|
rest ??= Array.Empty<byte>();
|
||||||
|
var result = new byte[rest.Length + 1];
|
||||||
|
result[0] = first;
|
||||||
|
Buffer.BlockCopy(rest, 0, result, 1, rest.Length);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 循环读取设备回传的裸二进制数据,直到收到 END 指示或终止符为止(参考 ADP TCP “读到没有为止” 策略)。
|
||||||
|
/// <para>无参 <c>RawIO.Read()</c> 只返回首个内部缓冲(约 1KB)即结束,会把大文件(如 PNG 截图)截断;
|
||||||
|
/// 本方法改用 <c>RawIO.Read(count, out ReadStatus)</c> 分块读取,依据 <see cref="Ivi.Visa.ReadStatus"/> 判定结束:
|
||||||
|
/// <c>EndReceived</c> / <c>TerminationCharacterEncountered</c> 表示传输完成,
|
||||||
|
/// <c>MaximumCountReached</c> 表示本次读满、可能仍有数据需继续读取。</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="prevTimeout">调用前的 I/O 超时(毫秒);大块传输期间在此放宽,读取结束后由调用方 finally 统一恢复。</param>
|
||||||
|
/// <returns>设备回传的完整裸二进制字节。</returns>
|
||||||
|
private byte[] ReadRawUntilEnd(int prevTimeout)
|
||||||
|
{
|
||||||
|
// 裸二进制可能含 0x0A,全程禁用终止符;大块传输放宽超时,避免 IOTimeoutException
|
||||||
|
_session!.TerminationCharacterEnabled = false;
|
||||||
|
_session!.TimeoutMilliseconds = Math.Max(prevTimeout, 30000);
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
const long chunkSize = 65536; // 64KB 分块,兼顾吞吐与内存占用
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
byte[] chunk;
|
||||||
|
Ivi.Visa.ReadStatus status;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
chunk = _session.RawIO.Read(chunkSize, out status);
|
||||||
|
}
|
||||||
|
catch (Exception) when (ms.Length > 0)
|
||||||
|
{
|
||||||
|
// 已收到部分数据后再发生超时等异常:视为传输结束,返回已收数据
|
||||||
|
// (兼容个别不发送 END 指示的 TCPIP SOCKET 链路)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk != null && chunk.Length > 0)
|
||||||
|
ms.Write(chunk, 0, chunk.Length);
|
||||||
|
|
||||||
|
// 收到 END 或终止符 = 设备已发完,传输结束
|
||||||
|
if (status == Ivi.Visa.ReadStatus.EndReceived ||
|
||||||
|
status == Ivi.Visa.ReadStatus.TerminationCharacterEncountered)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 缓冲读满但未见 END = 后面可能还有数据,继续下一轮读取
|
||||||
|
if (status == Ivi.Visa.ReadStatus.MaximumCountReached)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// 其余状态且本轮未读到任何字节:判定结束,避免无数据时空转
|
||||||
|
if (chunk == null || chunk.Length == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeSession();
|
||||||
|
_commLock?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NModbus" Version="3.0.83" />
|
<PackageReference Include="NModbus" Version="3.0.83" />
|
||||||
<PackageReference Include="NModbus.Serial" Version="3.0.83" />
|
<PackageReference Include="NModbus.Serial" Version="3.0.83" />
|
||||||
|
<PackageReference Include="NationalInstruments.Visa" Version="25.3.0.11" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using DeviceCommand.Base;
|
using DeviceCommand.Base;
|
||||||
using Model.Models;
|
using Model.Models;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -39,6 +40,26 @@ namespace DeviceCommand.Devices
|
|||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从设备返回的 SCPI 响应字符串中提取全部数值并按顺序转换为 double 数组。
|
||||||
|
/// 用于解析形如 "1.2,3.4,5.6" 的多值响应(如谐波振幅数组 MEAS:HARM:ARR?)。
|
||||||
|
/// 兼容逗号/分号/空格分隔及科学计数法。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">设备返回的原始字符串</param>
|
||||||
|
/// <returns>按出现顺序排列的数值数组,无有效数值时返回空数组</returns>
|
||||||
|
private static double[] ExtractDoubleArray(string raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<double>();
|
||||||
|
|
||||||
|
var values = new List<double>();
|
||||||
|
foreach (Match m in Regex.Matches(raw, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?"))
|
||||||
|
{
|
||||||
|
if (double.TryParse(m.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double v))
|
||||||
|
values.Add(v);
|
||||||
|
}
|
||||||
|
return values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
#region 1. 基础系统与公共指令
|
#region 1. 基础系统与公共指令
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -65,6 +86,17 @@ namespace DeviceCommand.Devices
|
|||||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 命令流延时 (*D)
|
||||||
|
/// <para>在 SCPI 命令流中插入一段延时(单位: ms),常用于 LIST 参数下发完成后、切换 OUTP:MODE 之前等待仪器就绪</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="milliseconds">延时时长 (ms)</param>
|
||||||
|
public virtual async Task 延时Async(double milliseconds, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string valStr = milliseconds.ToString("0.###", CultureInfo.InvariantCulture);
|
||||||
|
await SendAsync($"*D {valStr}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 2. 基础输出与耦合控制 (OUTPut 子系统)
|
#region 2. 基础输出与耦合控制 (OUTPut 子系统)
|
||||||
@@ -161,6 +193,37 @@ namespace DeviceCommand.Devices
|
|||||||
await SendAsync($"PHASE:THREE {relation}{ScpiDelimiter}", ct);
|
await SendAsync($"PHASE:THREE {relation}{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置相位耦合模式 (INST:COUP)
|
||||||
|
/// <para>ALL: 后续命令发送至所有相位; NONE: 仅发送至 NSEL 选定的相位</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="coupling">耦合模式: ALL | NONE</param>
|
||||||
|
public virtual async Task 设置相位耦合Async(string coupling, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"INST:COUP {coupling}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按编号选择输出相位 (INST:NSEL)
|
||||||
|
/// <para>仅在 INST:COUP NONE 时生效,后续 SOURCE 命令将作用于选定相位</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="phase">相位编号: 1(A相) | 2(B相) | 3(C相)</param>
|
||||||
|
public virtual async Task 选择相位Async(int phase, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"INST:NSEL {phase}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置参数编辑方式 (INST:EDIT)
|
||||||
|
/// <para>EACH: 逐相独立编辑,配合 <see cref="选择相位Async"/> 分别为 A/B/C 三相下发各自的 LIST/参数;
|
||||||
|
/// ALL: 三相统一编辑,一次下发同时作用于三相</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">编辑方式: EACH | ALL</param>
|
||||||
|
public virtual async Task 设置编辑方式Async(string mode, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"INST:EDIT {mode}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置输出开启相位角 (PHASE:ON)
|
/// 设置输出开启相位角 (PHASE:ON)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -387,6 +450,110 @@ namespace DeviceCommand.Devices
|
|||||||
await SendAsync($"TRIG {state}{ScpiDelimiter}", ct);
|
await SendAsync($"TRIG {state}{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询列表触发执行状态 (TRIG:STATE?)
|
||||||
|
/// <para>返回 ON 表示 LIST 正在执行,OFF 表示已执行完毕;
|
||||||
|
/// 重新下发 LIST 参数前应先轮询本命令等待其变为 OFF,避免执行中被改写</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>true: 正在执行(ON); false: 已结束(OFF)</returns>
|
||||||
|
public virtual async Task<bool> 查询触发状态Async(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string resp = await WriteReadAsync($"TRIG:STATE?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return resp != null && resp.Trim().StartsWith("ON", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 7. 谐波量测 (Harmonic Sense 子系统)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置谐波分析的测量来源 (CONFigure:HARMonic:SOURce)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">VOLT: 输出电压; CURR: 输出电流</param>
|
||||||
|
public virtual async Task 设置谐波量测源Async(string source, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 有效参数: VOLT, CURR
|
||||||
|
await SendAsync($"CONF:HARM:SOUR {source}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置谐波量测结果显示方式 (CONFigure:HARMonic:TIMES)
|
||||||
|
/// <para>SINGLE: 保留单次测量结果,约需 3 秒; CONTINUE: 持续更新,约需 10 秒稳定</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="times">显示方式: SINGLE | CONTINUE</param>
|
||||||
|
public virtual async Task 设置谐波量测方式Async(string times, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 有效参数: SINGLE, CONTINUE
|
||||||
|
await SendAsync($"CONF:HARM:TIM {times}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置每个谐波阶的数据格式 (CONFigure:HARMonic:PARameter)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">VALUE: 绝对值; PERCENT: 基频百分率</param>
|
||||||
|
public virtual async Task 设置谐波数据格式Async(string parameter, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 有效参数: VALUE, PERCENT
|
||||||
|
await SendAsync($"CONF:HARM:PAR {parameter}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置谐波分析的原始波形基频 (CONFigure:HARMonic:FREQuency)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="frequency">基频: 50 或 60 (Hz)</param>
|
||||||
|
public virtual async Task 设置谐波基频Async(int frequency, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"CONF:HARM:FREQ {frequency}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开启/关闭谐波量测 (SENSe:HARMonic)
|
||||||
|
/// <para>每次新的 Fetch/Measure 前必须先 ON,约需 3 秒得到结果;
|
||||||
|
/// 若要量测其他数据,须先设回 OFF。分相量测配合 INST:COUP NONE + INST:NSEL 逐相进行</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isOn">true: 开启谐波量测(ON); false: 关闭(OFF)</param>
|
||||||
|
public virtual async Task 设置谐波量测开关Async(bool isOn, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string state = isOn ? "ON" : "OFF";
|
||||||
|
await SendAsync($"SENS:HARM {state}{ScpiDelimiter}", ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询总谐波失真 (MEASure:HARMonic:THD?)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>总谐波失真 THD (单位: %,保留两位小数)</returns>
|
||||||
|
public virtual async Task<double> 查询谐波总失真Async(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string resp = await WriteReadAsync($"MEAS:HARM:THD?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询输出电压/电流的基频幅值 (MEASure:HARMonic:FUNDamental?)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>基频幅值 (单位依 CONF:HARM:PAR 为 V/A 绝对值或百分率)</returns>
|
||||||
|
public virtual async Task<double> 查询谐波基频幅值Async(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string resp = await WriteReadAsync($"MEAS:HARM:FUND?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询所有谐波阶的振幅数组 (MEASure:HARMonic:ARRay?)
|
||||||
|
/// <para>手册 5.7 谐波测量:可量测 2~50 阶谐波,来源可选输出电压(VOLT)或电流(CURR)。
|
||||||
|
/// 注意:仅返回各阶振幅,不含各阶相位角,故据此重构的波形形状为"同相叠加"近似</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>各次谐波振幅数组(按阶次升序;单位依 CONF:HARM:PAR)</returns>
|
||||||
|
public virtual async Task<double[]> 查询谐波振幅数组Async(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string resp = await WriteReadAsync($"MEAS:HARM:ARR?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDoubleArray(resp);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <param name="data">开关状态(true: 吸合/开启, false: 断开/关闭)</param>
|
/// <param name="data">开关状态(true: 吸合/开启, false: 断开/关闭)</param>
|
||||||
public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
|
public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
|
||||||
{
|
{
|
||||||
|
确保已连接();
|
||||||
await Modbus.WriteSingleCoilAsync(站号, 开始地址, data);
|
await Modbus.WriteSingleCoilAsync(站号, 开始地址, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <param name="datas">要写入的开关状态数组,按顺序从开始地址写入</param>
|
/// <param name="datas">要写入的开关状态数组,按顺序从开始地址写入</param>
|
||||||
public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
|
public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
|
||||||
{
|
{
|
||||||
|
确保已连接();
|
||||||
var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16);
|
var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16);
|
||||||
for (int i = 0; i < datas.Length; i++)
|
for (int i = 0; i < datas.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -65,6 +67,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <param name="开始地址">线圈地址(0 起始)</param>
|
/// <param name="开始地址">线圈地址(0 起始)</param>
|
||||||
public async Task 读输出开关(byte 站号, ushort 开始地址)
|
public async Task 读输出开关(byte 站号, ushort 开始地址)
|
||||||
{
|
{
|
||||||
|
确保已连接();
|
||||||
await Modbus.ReadCoilsAsync(站号, 开始地址, 1);
|
await Modbus.ReadCoilsAsync(站号, 开始地址, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,20 @@ namespace DeviceCommand.Devices
|
|||||||
/// <returns>线圈状态数组,true 表示吸合/开启</returns>
|
/// <returns>线圈状态数组,true 表示吸合/开启</returns>
|
||||||
public async Task<bool[]> 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量)
|
public async Task<bool[]> 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量)
|
||||||
{
|
{
|
||||||
|
确保已连接();
|
||||||
return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量);
|
return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确认 Modbus 主站已建立(设备连接成功)。
|
||||||
|
/// 未连接时抛出带明确提示的异常,替代对 null 主站的 NullReferenceException,
|
||||||
|
/// 供上层(测试步骤 / 报警蜂鸣器)捕获并记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">Modbus 主站尚未建立时抛出</exception>
|
||||||
|
private void 确保已连接()
|
||||||
|
{
|
||||||
|
if (Modbus == null)
|
||||||
|
throw new InvalidOperationException("IO 板卡尚未连接(Modbus 主站未建立),无法执行输出控制操作,请确认设备连接状态。");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using Model.Models;
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -55,10 +54,11 @@ namespace DeviceCommand.Devices
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tektronix 示波器 (MSO 4/5/6 系列),支持采集控制、通道设置、自动测量与截图导出
|
/// Tektronix 示波器 (MSO 4/5/6 系列),支持采集控制、通道设置、自动测量与截图导出。
|
||||||
|
/// 传输层为 VISA(资源字符串寻址,如 TCPIP0::IP::inst0::INSTR),SCPI 指令集不变。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ACPCommand]
|
[ACPCommand]
|
||||||
public class TektronixMSO : Tcp
|
public class TektronixMSO : Visa
|
||||||
{
|
{
|
||||||
// SCPI 指令结束符 (消息终止符需使用 LF)
|
// SCPI 指令结束符 (消息终止符需使用 LF)
|
||||||
private const string ScpiDelimiter = "\n";
|
private const string ScpiDelimiter = "\n";
|
||||||
@@ -102,7 +102,7 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public TektronixMSO(TcpConfig config) : base(config)
|
public TektronixMSO(VisaConfig config) : base(config)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,103 +327,36 @@ namespace DeviceCommand.Devices
|
|||||||
#region 6. 屏幕截图与文件系统 (Hard Copy & File System)
|
#region 6. 屏幕截图与文件系统 (Hard Copy & File System)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 捕获示波器屏幕并保存到上位机本地路径
|
/// 捕获示波器当前屏幕并保存为 PNG 到上位机本地路径 (SAVe:IMAGe / FILESystem:READFile 指令)。
|
||||||
|
/// <para>执行流程:① SAVe:IMAGe 将屏幕截图写入示波器本地临时文件;② *OPC? 阻塞等待写盘完成;
|
||||||
|
/// ③ FILESystem:READFile 以 IEEE 488.2 定长块(#9<长度><数据>)回传文件,基类自动剥离块头后返回纯 PNG 字节;
|
||||||
|
/// ④ 写入上位机本地磁盘;⑤ 删除示波器临时文件,保持设备存储干净。</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="上位机文件路径">保存到电脑本地的文件路径 (例如: @"C:\temp\screen.png")</param>
|
/// <param name="上位机文件路径">保存到电脑本地的文件路径 (例如: @"C:\temp\screen.png")</param>
|
||||||
/// <param name="ct">异步取消令牌</param>
|
/// <param name="ct">异步取消令牌</param>
|
||||||
public virtual async Task 保存屏幕截图(string 上位机文件路径, CancellationToken ct = default)
|
public virtual async Task 保存屏幕截图(string 上位机文件路径, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string 临时文件 = "E:/temp1.png";
|
// 示波器端临时文件:截图先落到设备本地,再回传上位机
|
||||||
|
const string 临时文件 = "E:/Temp.png";
|
||||||
|
|
||||||
// 1. 直接保存截图到示波器U盘 (自动识别 .png 扩展名)
|
// 1. 命令示波器把当前屏幕保存为 PNG 到设备本地临时文件
|
||||||
await SendAsync($"SAVe:IMAGe \"{临时文件}\"{ScpiDelimiter}", ct);
|
await SendAsync($"SAVe:IMAGe \"{临时文件}\"{ScpiDelimiter}", ct);
|
||||||
|
|
||||||
// 2. 发送 *OPC? 并等待返回 1,确保图像文件已经完全写入磁盘
|
// 2. 发送 *OPC? 并等待返回,确保图像文件已完全写入磁盘
|
||||||
await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
|
||||||
// 3. 将文件内容读取到当前通信接口
|
// 3. 读取示波器临时文件回传的 IEEE 488.2 定长块(基类自动剥离 #9 块头,返回纯 PNG 字节)
|
||||||
await SendAsync($"FILESystem:READFile \"{临时文件}\"{ScpiDelimiter}", ct);
|
byte[] imageBytes = await ReadAllBytesAsync($"FILESystem:READFile \"{临时文件}\"{ScpiDelimiter}", ct);
|
||||||
|
|
||||||
// 4. 提取底层的二进制块 (Block Data)
|
// 4. 确保本地目录存在后,将二进制字节流写入上位机磁盘
|
||||||
byte[] imageBytes = await ReadBinaryBlockAsync(ct);
|
string? 目录 = Path.GetDirectoryName(上位机文件路径);
|
||||||
|
if (!string.IsNullOrEmpty(目录)) Directory.CreateDirectory(目录);
|
||||||
// 5. 将提取的二进制字节流写入上位机本地磁盘
|
|
||||||
await File.WriteAllBytesAsync(上位机文件路径, imageBytes, ct);
|
await File.WriteAllBytesAsync(上位机文件路径, imageBytes, ct);
|
||||||
|
|
||||||
// 6. 删除示波器上的临时文件,保持设备存储干净
|
// 5. 删除示波器上的临时文件,保持设备存储干净
|
||||||
await SendAsync($"FILESystem:DELEte \"{临时文件}\"{ScpiDelimiter}", ct);
|
await SendAsync($"FILESystem:DELEte \"{临时文件}\"{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ⚠️ 请在这里将您的网络流对接起来!
|
|
||||||
/// 依据您 Tcp 基类的设计,返回用于读取底层字节流的 Stream。
|
|
||||||
/// </summary>
|
|
||||||
protected virtual Stream GetNetworkStream()
|
|
||||||
{
|
|
||||||
// 【修改提示】:
|
|
||||||
// 如果您的基类暴露了 TcpClient,请取消注释并使用这一行:
|
|
||||||
// return this.TcpClient.GetStream();
|
|
||||||
|
|
||||||
// 如果您的基类直接叫 _stream 或 Stream,请返回它:
|
|
||||||
// return this.Stream;
|
|
||||||
|
|
||||||
// 假如不知道怎么填,请查看 Tcp 类的源码。
|
|
||||||
throw new NotImplementedException("请在此方法中返回基类的 NetworkStream 或 Socket 流对象。");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 辅助方法:读取 IEEE 488.2 标准的二进制块数据 (Block Data)
|
|
||||||
/// </summary>
|
|
||||||
protected virtual async Task<byte[]> ReadBinaryBlockAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var stream = GetNetworkStream();
|
|
||||||
byte[] singleByte = new byte[1];
|
|
||||||
|
|
||||||
// 1. 逐字节读取,直到读到起始符 '#'
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
int read = await stream.ReadAsync(singleByte, 0, 1, ct);
|
|
||||||
if (read == 0) throw new EndOfStreamException("通信中断:未找到二进制块起始符 '#'");
|
|
||||||
if (singleByte[0] == (byte)'#') break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 读取 1 个字符 <NZDig>,它代表接下来有多少个字符表示数据长度
|
|
||||||
await stream.ReadAsync(singleByte, 0, 1, ct);
|
|
||||||
char nzDigChar = (char)singleByte[0];
|
|
||||||
if (!char.IsDigit(nzDigChar)) throw new InvalidDataException($"无效的块头标识: {nzDigChar}");
|
|
||||||
int lengthBytesCount = int.Parse(nzDigChar.ToString());
|
|
||||||
|
|
||||||
// 3. 读取 <NZDig> 所指示的长度字符,将其解析为整数 N
|
|
||||||
byte[] lengthBytes = new byte[lengthBytesCount];
|
|
||||||
int currentRead = 0;
|
|
||||||
while (currentRead < lengthBytesCount)
|
|
||||||
{
|
|
||||||
int read = await stream.ReadAsync(lengthBytes, currentRead, lengthBytesCount - currentRead, ct);
|
|
||||||
if (read == 0) throw new EndOfStreamException("通信中断:读取数据长度失败");
|
|
||||||
currentRead += read;
|
|
||||||
}
|
|
||||||
int dataLength = int.Parse(Encoding.ASCII.GetString(lengthBytes));
|
|
||||||
|
|
||||||
// 4. 从流中精确读取 N 个字节 <DChar>...,这就是完整的图像数据
|
|
||||||
byte[] imageData = new byte[dataLength];
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
while (totalBytesRead < dataLength)
|
|
||||||
{
|
|
||||||
int read = await stream.ReadAsync(imageData, totalBytesRead, dataLength - totalBytesRead, ct);
|
|
||||||
if (read == 0) throw new EndOfStreamException("通信中断:读取图像数据不完整");
|
|
||||||
totalBytesRead += read;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 结尾可能包含 <EOM> (例如 LF \n),将其读取并丢弃以清空缓冲区
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await stream.ReadAsync(singleByte, 0, 1, ct);
|
|
||||||
}
|
|
||||||
catch { /* 忽略末尾符读取的可能超时 */ }
|
|
||||||
|
|
||||||
return imageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 7. 状态与错误队列 (Status and Error)
|
#region 7. 状态与错误队列 (Status and Error)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace DeviceEditModule
|
|||||||
containerRegistry.RegisterForNavigation<MCc30WView>("MCc30WView");
|
containerRegistry.RegisterForNavigation<MCc30WView>("MCc30WView");
|
||||||
containerRegistry.RegisterForNavigation<RLT1000View>("RLT1000View");
|
containerRegistry.RegisterForNavigation<RLT1000View>("RLT1000View");
|
||||||
containerRegistry.RegisterForNavigation<S7200View>("S7200View");
|
containerRegistry.RegisterForNavigation<S7200View>("S7200View");
|
||||||
|
containerRegistry.RegisterForNavigation<TektronixMSOView>("TektronixMSOView");
|
||||||
containerRegistry.Register<DG1000ZViewModel>();
|
containerRegistry.Register<DG1000ZViewModel>();
|
||||||
containerRegistry.Register<IT6720ViewModel>();
|
containerRegistry.Register<IT6720ViewModel>();
|
||||||
containerRegistry.Register<PW8001ViewModel>();
|
containerRegistry.Register<PW8001ViewModel>();
|
||||||
@@ -31,6 +32,7 @@ namespace DeviceEditModule
|
|||||||
containerRegistry.Register<MCc30WViewModel>();
|
containerRegistry.Register<MCc30WViewModel>();
|
||||||
containerRegistry.Register<RLT1000ViewModel>();
|
containerRegistry.Register<RLT1000ViewModel>();
|
||||||
containerRegistry.Register<S7200ViewModel>();
|
containerRegistry.Register<S7200ViewModel>();
|
||||||
|
containerRegistry.Register<TektronixMSOViewModel>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using DeviceCommand.Devices;
|
|||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@@ -30,6 +31,12 @@ namespace DeviceEditModule.ViewModels
|
|||||||
private double _voltage; public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
|
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 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); }
|
private string _waveform = "SINE"; public string Waveform { get => _waveform; set => SetProperty(ref _waveform, value); }
|
||||||
|
|
||||||
|
/// <summary>相位选择列表: 0=ALL(三相同时), 1=A相, 2=B相, 3=C相</summary>
|
||||||
|
public List<string> PhaseOptions { get; } = new() { "ALL (三相)", "A相 (Φ1)", "B相 (Φ2)", "C相 (Φ3)" };
|
||||||
|
private int _selectedPhaseIndex;
|
||||||
|
/// <summary>当前选中的相位索引 (0=ALL, 1=A, 2=B, 3=C)</summary>
|
||||||
|
public int SelectedPhaseIndex { get => _selectedPhaseIndex; set => SetProperty(ref _selectedPhaseIndex, value); }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 测量结果
|
#region 测量结果
|
||||||
@@ -60,18 +67,34 @@ namespace DeviceEditModule.ViewModels
|
|||||||
Reset = new DelegateCommand(async () => await Exec(async () => { await _dev!.复位Async(Ct()); Log("设备已复位"); }));
|
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("输出已开启"); }));
|
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("输出已关闭"); }));
|
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"); }));
|
SetV = new DelegateCommand(async () => await Exec(async () =>
|
||||||
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}"); }));
|
await ApplyPhaseAsync();
|
||||||
|
await _dev!.设置交流电压Async(Voltage, Ct());
|
||||||
|
Log($"电压={Voltage}V → {PhaseLabel}");
|
||||||
|
}));
|
||||||
|
SetF = new DelegateCommand(async () => await Exec(async () =>
|
||||||
|
{
|
||||||
|
await ApplyPhaseAsync();
|
||||||
|
await _dev!.设置频率Async(Frequency, Ct());
|
||||||
|
Log($"频率={Frequency}Hz → {PhaseLabel}");
|
||||||
|
}));
|
||||||
|
SetWave = new DelegateCommand(async () => await Exec(async () =>
|
||||||
|
{
|
||||||
|
await ApplyPhaseAsync();
|
||||||
|
await _dev!.设置波形Async(Waveform, Ct());
|
||||||
|
Log($"波形={Waveform} → {PhaseLabel}");
|
||||||
|
}));
|
||||||
SetThreePhase = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置单三相模式Async(true, Ct()); Log("三相模式"); }));
|
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("单相模式"); }));
|
SetSinglePhase = new DelegateCommand(async () => await Exec(async () => { await _dev!.设置单三相模式Async(false, Ct()); Log("单相模式"); }));
|
||||||
QMeas = new DelegateCommand(async () => await Exec(async () =>
|
QMeas = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
|
await ApplyPhaseAsync();
|
||||||
MeasuredVoltage = await _dev!.读取交流电压Async(Ct());
|
MeasuredVoltage = await _dev!.读取交流电压Async(Ct());
|
||||||
MeasuredCurrent = await _dev!.读取交流电流Async(Ct());
|
MeasuredCurrent = await _dev!.读取交流电流Async(Ct());
|
||||||
MeasuredFrequency = await _dev!.读取频率Async(Ct());
|
MeasuredFrequency = await _dev!.读取频率Async(Ct());
|
||||||
MeasuredPower = await _dev!.读取真实功率Async(Ct());
|
MeasuredPower = await _dev!.读取真实功率Async(Ct());
|
||||||
Log($"测量→V:{MeasuredVoltage} I:{MeasuredCurrent} F:{MeasuredFrequency} P:{MeasuredPower}");
|
Log($"测量[{PhaseLabel}]→V:{MeasuredVoltage} I:{MeasuredCurrent} F:{MeasuredFrequency} P:{MeasuredPower}");
|
||||||
}));
|
}));
|
||||||
Initialize();
|
Initialize();
|
||||||
}
|
}
|
||||||
@@ -106,6 +129,32 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 辅助
|
#region 辅助
|
||||||
|
|
||||||
|
/// <summary>当前选中相位的显示标签</summary>
|
||||||
|
private string PhaseLabel => SelectedPhaseIndex switch
|
||||||
|
{
|
||||||
|
1 => "A相",
|
||||||
|
2 => "B相",
|
||||||
|
3 => "C相",
|
||||||
|
_ => "ALL"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 SelectedPhaseIndex 发送 INST:COUP / INST:NSEL 命令切换相位上下文。
|
||||||
|
/// <para>索引 0 → INST:COUP ALL (三相同时);索引 1~3 → INST:COUP NONE + INST:NSEL n</para>
|
||||||
|
/// </summary>
|
||||||
|
private async Task ApplyPhaseAsync()
|
||||||
|
{
|
||||||
|
if (SelectedPhaseIndex == 0)
|
||||||
|
{
|
||||||
|
await _dev!.设置相位耦合Async("ALL", Ct());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _dev!.设置相位耦合Async("NONE", Ct());
|
||||||
|
await _dev!.选择相位Async(SelectedPhaseIndex, Ct());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||||
|
|
||||||
private async Task Exec(Func<Task> action)
|
private async Task Exec(Func<Task> action)
|
||||||
|
|||||||
@@ -94,6 +94,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<GroupBox Header="参数设置" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
<GroupBox Header="参数设置" Margin="0,0,0,8" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
<StackPanel Margin="4">
|
<StackPanel Margin="4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="相位选择" Style="{StaticResource ParamLabel}"/>
|
||||||
|
<ComboBox Width="130" VerticalAlignment="Center" Margin="4,0"
|
||||||
|
ItemsSource="{Binding PhaseOptions}"
|
||||||
|
SelectedIndex="{Binding SelectedPhaseIndex}"/>
|
||||||
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace MainModule.ViewModels
|
|||||||
private bool IsInitialized =false;
|
private bool IsInitialized =false;
|
||||||
private bool _isRunningErrorSteps = false;
|
private bool _isRunningErrorSteps = false;
|
||||||
private SubscriptionToken? _alarmToken;
|
private SubscriptionToken? _alarmToken;
|
||||||
|
private SubscriptionToken? _silenceToken;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 属性
|
#region 属性
|
||||||
@@ -87,7 +88,7 @@ namespace MainModule.ViewModels
|
|||||||
RefreshCommand = new DelegateCommand(OnRefresh);
|
RefreshCommand = new DelegateCommand(OnRefresh);
|
||||||
BackToProtocolCommand = new DelegateCommand(OnBackToProtocol);
|
BackToProtocolCommand = new DelegateCommand(OnBackToProtocol);
|
||||||
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
||||||
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Subscribe(async (scope) => await SilenceBuzzer(scope));
|
_silenceToken = _eventAggregator.GetEvent<SilenceBuzzerEvent>().Subscribe(async (scope) => await SilenceBuzzer(scope));
|
||||||
_alarmToken = _eventAggregator.GetEvent<AlarmEvent>().Subscribe(OnAlarmTriggered);
|
_alarmToken = _eventAggregator.GetEvent<AlarmEvent>().Subscribe(OnAlarmTriggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +96,8 @@ namespace MainModule.ViewModels
|
|||||||
{
|
{
|
||||||
if (_deviceManager.IOGroup == null) return;
|
if (_deviceManager.IOGroup == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
if (scope == "default")
|
if (scope == "default")
|
||||||
{
|
{
|
||||||
for(int i = 1; i <= 8; i++)
|
for(int i = 1; i <= 8; i++)
|
||||||
@@ -112,6 +115,12 @@ namespace MainModule.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 消音为尽力而为动作:IO 板卡未连接或通信失败时仅记录日志,避免切换页面/点击消音时崩溃
|
||||||
|
LoggerHelper.Error($"[{scope}] 蜂鸣器消音失败:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AlarmEvent 回调:
|
/// AlarmEvent 回调:
|
||||||
@@ -127,9 +136,17 @@ namespace MainModule.ViewModels
|
|||||||
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0;
|
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0;
|
||||||
if (台架号 < 1 || 台架号 > 8) return;
|
if (台架号 < 1 || 台架号 > 8) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
// 上下限 / 上下极限 均触发蜂鸣器报警
|
// 上下限 / 上下极限 均触发蜂鸣器报警
|
||||||
await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, true);
|
await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, true);
|
||||||
LoggerHelper.WarnWithNotify(args.Scope, $"[{args.Scope}] 报警触发: {args.Status} ({args.Fingerprint}),蜂鸣器已开启。");
|
LoggerHelper.WarnWithNotify(args.Scope, $"[{args.Scope}] 报警触发: {args.Status} ({args.Fingerprint}),蜂鸣器已开启。");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// async void 内绝不能抛出异常:蜂鸣器为尽力而为动作,IO 板卡未连接/通信失败时仅记录日志,避免程序崩溃
|
||||||
|
LoggerHelper.Error($"[{args.Scope}] 蜂鸣器报警开启失败:{ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
// 上下极限额外触发异常流程
|
// 上下极限额外触发异常流程
|
||||||
if (args.Status == "超上极限" || args.Status == "超下极限")
|
if (args.Status == "超上极限" || args.Status == "超下极限")
|
||||||
@@ -205,7 +222,8 @@ namespace MainModule.ViewModels
|
|||||||
{
|
{
|
||||||
if (_alarmToken != null)
|
if (_alarmToken != null)
|
||||||
_eventAggregator.GetEvent<AlarmEvent>().Unsubscribe(_alarmToken);
|
_eventAggregator.GetEvent<AlarmEvent>().Unsubscribe(_alarmToken);
|
||||||
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Unsubscribe(async (scope) => await SilenceBuzzer(scope));
|
if (_silenceToken != null)
|
||||||
|
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Unsubscribe(_silenceToken);
|
||||||
// 2. 显式释放硬件资源(防止端口占用/死锁)
|
// 2. 显式释放硬件资源(防止端口占用/死锁)
|
||||||
if (_deviceManager is IDisposable disposableDevice)
|
if (_deviceManager is IDisposable disposableDevice)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Model.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VISA 通信参数(DeviceCommand 内部纯数据类,供设备类构造函数使用)。
|
||||||
|
/// 与 UIShare.UIViewModel.VisaConfigVM 字段一一对应,由 DeviceManager 在实例化时填充。
|
||||||
|
/// <para>VISA 通过统一的资源字符串寻址,无需区分 IP/端口:
|
||||||
|
/// TCPIP0::192.168.20.125::inst0::INSTR、USB0::0x0699::0x0408::C012345::INSTR、GPIB0::7::INSTR 等。</para>
|
||||||
|
/// </summary>
|
||||||
|
public class VisaConfig
|
||||||
|
{
|
||||||
|
/// <summary>VISA 资源字符串(由 NI-VISA 解析并自动选择对应的传输层)。</summary>
|
||||||
|
public string ResourceName { get; set; } = "TCPIP0::127.0.0.1::inst0::INSTR";
|
||||||
|
|
||||||
|
/// <summary>打开 VISA 会话的最长等待时间(毫秒)。</summary>
|
||||||
|
public int OpenTimeout { get; set; } = 3000;
|
||||||
|
|
||||||
|
/// <summary>读写 I/O 超时(毫秒),对应 VISA 的 TimeoutMilliseconds。</summary>
|
||||||
|
public int IOTimeout { get; set; } = 3000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,7 +157,9 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -438,7 +438,9 @@ namespace MonitorModule.ViewModels
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -233,7 +233,9 @@ namespace MonitorModule.ViewModels
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using PowerAdaptabilityModule.Views;
|
||||||
|
|
||||||
|
namespace PowerAdaptabilityModule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 电源适应性测试模块:Chroma61800 交流源三相输出控制、LIST 模式设置与三相数据实时波形展示。
|
||||||
|
/// 通过左侧菜单“电源适应性测试”导航进入(仅 default 作用域可用)。
|
||||||
|
/// </summary>
|
||||||
|
public class PowerAdaptabilityModule : IModule
|
||||||
|
{
|
||||||
|
public void OnInitialized(IContainerProvider containerProvider)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||||
|
{
|
||||||
|
containerRegistry.RegisterForNavigation<PowerAdaptabilityView>("PowerAdaptabilityView");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\UIShare\UIShare.csproj" />
|
||||||
|
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
|||||||
|
<UserControl x:Class="PowerAdaptabilityModule.Views.PowerAdaptabilityView"
|
||||||
|
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:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
|
xmlns:oxy="http://oxyplot.org/wpf"
|
||||||
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
|
d:DesignHeight="1080"
|
||||||
|
d:DesignWidth="1920">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Border Background="#F5F7FA">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="440" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ============ 左侧:Chroma 命令面板 ============ -->
|
||||||
|
<ScrollViewer Grid.Column="0"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
Margin="0,0,8,0">
|
||||||
|
<StackPanel>
|
||||||
|
|
||||||
|
<!-- 设备状态卡片 -->
|
||||||
|
<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="60" Height="4"
|
||||||
|
Margin="12,0,0,0"
|
||||||
|
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</materialDesign:Card>
|
||||||
|
|
||||||
|
<!-- 三相输出控制 -->
|
||||||
|
<GroupBox Header="三相输出控制" Margin="0,0,0,8"
|
||||||
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<StackPanel Margin="4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="开启输出" Command="{Binding OutputOnCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"
|
||||||
|
Background="#388E3C" Foreground="White" />
|
||||||
|
<Button Content="关闭输出" Command="{Binding OutputOffCommand}"
|
||||||
|
Style="{StaticResource WarnBtn}" />
|
||||||
|
<Button Content="复位" Command="{Binding ResetCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
<Button Content="查询 IDN" Command="{Binding QueryIdnCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="相模式" Style="{StaticResource ParamLabel}" />
|
||||||
|
<Button Content="三相" Command="{Binding SetThreePhaseModeCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
<Button Content="单相" Command="{Binding SetSinglePhaseModeCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="110" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding CouplingOptions}"
|
||||||
|
SelectedItem="{Binding CouplingMode}" />
|
||||||
|
<Button Content="应用" Command="{Binding ApplyCouplingCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="三相关系" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="110" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding RelationOptions}"
|
||||||
|
SelectedItem="{Binding ThreePhaseRelation}" />
|
||||||
|
<Button Content="应用" Command="{Binding ApplyRelationCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="输出继电器" Style="{StaticResource ParamLabel}" />
|
||||||
|
<Button Content="开启" Command="{Binding RelayOnCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
<Button Content="关闭" Command="{Binding RelayOffCommand}"
|
||||||
|
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="相位选择" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="130" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding PhaseOptions}"
|
||||||
|
SelectedIndex="{Binding SelectedPhaseIndex}" />
|
||||||
|
</StackPanel>
|
||||||
|
<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 SetVoltageCommand}"
|
||||||
|
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 SetFrequencyCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="波形" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="130" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding WaveformOptions}"
|
||||||
|
SelectedItem="{Binding Waveform}" />
|
||||||
|
<Button Content="设置" Command="{Binding SetWaveformCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- LIST 模式(三相独立) -->
|
||||||
|
<GroupBox Header="LIST 模式(三相独立)" Margin="0,0,0,8"
|
||||||
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<StackPanel Margin="4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="测试项" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="230" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding PresetOptions}"
|
||||||
|
SelectedIndex="{Binding SelectedPresetIndex}" />
|
||||||
|
<Button Content="载入" Command="{Binding LoadPresetCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="名称" Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Style="{StaticResource NumInput}" Width="150"
|
||||||
|
ToolTip="保存时作为文件名(写入 D:\acp\源测试);选中已有测试项会自动填充,可改名另存"
|
||||||
|
Text="{Binding PresetName, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<Button Content="保存" Command="{Binding SavePresetCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"
|
||||||
|
Background="#388E3C" Foreground="White" />
|
||||||
|
<Button Content="删除" Command="{Binding DeletePresetCommand}"
|
||||||
|
Style="{StaticResource WarnBtn}"
|
||||||
|
ToolTip="删除下拉框当前选中的测试项文件(带二次确认)" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox Margin="4,0,4,2" FontSize="11" Foreground="#1565C0"
|
||||||
|
Height="46" TextWrapping="Wrap" AcceptsReturn="True"
|
||||||
|
VerticalScrollBarVisibility="Auto" VerticalContentAlignment="Top"
|
||||||
|
ToolTip="测试项说明(随文件一并保存)"
|
||||||
|
Text="{Binding PresetNote, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBlock Text="{Binding PresetStorageHint}"
|
||||||
|
FontSize="11" Foreground="#757575"
|
||||||
|
Margin="4,0,4,4" TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<Separator Margin="0,4" />
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="操作模态" Style="{StaticResource ParamLabel}" />
|
||||||
|
<Button Content="FIXED 固定" Command="{Binding SetFixedModeCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
<Button Content="进入 LIST" Command="{Binding EnterListModeCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="执行次数" Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Style="{StaticResource NumInput}" Width="60"
|
||||||
|
ToolTip="0 = 无限循环"
|
||||||
|
Text="{Binding ListCount, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<TextBlock Text="触发" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="88" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding ListTriggerOptions}"
|
||||||
|
SelectedItem="{Binding ListTrigger}" />
|
||||||
|
<TextBlock Text="时基" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="80" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding ListTimeBaseOptions}"
|
||||||
|
SelectedItem="{Binding ListTimeBase}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator Margin="0,6" />
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<TextBlock Text="编辑相位" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="100" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding ListPhaseOptions}"
|
||||||
|
SelectedIndex="{Binding ListEditPhaseIndex}" />
|
||||||
|
<TextBlock Text="该相波形" Style="{StaticResource ParamLabel}" />
|
||||||
|
<ComboBox Width="80" Height="32" FontSize="12" Margin="4,0"
|
||||||
|
ItemsSource="{Binding WaveformOptions}"
|
||||||
|
SelectedItem="{Binding ListWaveform}" />
|
||||||
|
</StackPanel>
|
||||||
|
<Button Content="复制当前相到三相(B/C 相位角自动 ±120°)"
|
||||||
|
Command="{Binding CopyPhaseToThreeCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"
|
||||||
|
HorizontalAlignment="Left" Margin="0,4" />
|
||||||
|
<TextBlock Text="序列设置(英文逗号分隔,留空则不下发;作用于上方选中的相位)"
|
||||||
|
FontSize="12" Foreground="#616161" Margin="4,2" TextWrapping="Wrap" />
|
||||||
|
<Grid Margin="0,2">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="92" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="相位角度"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:DEGR,单位 °,如 0,90,0;三相基准 0/120/240"
|
||||||
|
Text="{Binding ListDegrText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="AC起始电压"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:VOLT:AC:STAR,单位 V,如 230,11.5,230"
|
||||||
|
Text="{Binding ListVoltAcStartText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="AC结束电压"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:VOLT:AC:END,单位 V,如 230,11.5,230"
|
||||||
|
Text="{Binding ListVoltAcEndText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="DC起始电压"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:VOLT:DC:STAR,单位 V,如 10,10,10"
|
||||||
|
Text="{Binding ListVoltDcStartText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="DC结束电压"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:VOLT:DC:END,单位 V,如 10,10,10"
|
||||||
|
Text="{Binding ListVoltDcEndText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="起始频率"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:FREQ:STAR,单位 Hz,如 50,50,50"
|
||||||
|
Text="{Binding ListFreqStartText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="0" Text="结束频率"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="6" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:FREQ:END,单位 Hz,如 50,50,50"
|
||||||
|
Text="{Binding ListFreqEndText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="7" Grid.Column="0" Text="静止时间"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="7" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:DWEL,单位 ms,如 20000,280,20000"
|
||||||
|
Text="{Binding ListDwellText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="8" Grid.Column="0" Text="波形顺序"
|
||||||
|
Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Grid.Row="8" Grid.Column="1" Style="{StaticResource NumInput}"
|
||||||
|
Width="240" ToolTip="LIST:SHAP,A / B 序列,如 A,A,A(可留空)"
|
||||||
|
Text="{Binding ListShapeText, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Separator Margin="0,6" />
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="下发三相并启动 LIST" Command="{Binding DownloadThreePhaseListCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"
|
||||||
|
Background="#388E3C" Foreground="White" />
|
||||||
|
<Button Content="停止 LIST" Command="{Binding ListStopCommand}"
|
||||||
|
Style="{StaticResource WarnBtn}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 数据采集 -->
|
||||||
|
<GroupBox Header="数据采集" Margin="0,0,0,8"
|
||||||
|
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<StackPanel Margin="4">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<Button Content="开始采集" Command="{Binding StartSamplingCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}"
|
||||||
|
Background="#388E3C" Foreground="White" />
|
||||||
|
<Button Content="停止采集" Command="{Binding StopSamplingCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
<Button Content="清空图表" Command="{Binding ClearChartsCommand}"
|
||||||
|
Style="{StaticResource CmdBtn}" />
|
||||||
|
<TextBlock VerticalAlignment="Center" FontSize="12" Margin="8,0,0,0">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="采集中…" />
|
||||||
|
<Setter Property="Foreground" Value="#4CAF50" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSampling}" Value="False">
|
||||||
|
<Setter Property="Text" Value="采集已停止" />
|
||||||
|
<Setter Property="Foreground" Value="#9E9E9E" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
|
<CheckBox Content="采集频率" IsChecked="{Binding IsFrequencySamplingEnabled}"
|
||||||
|
VerticalAlignment="Center" Margin="4,0,10,0"
|
||||||
|
ToolTip="频率一般无需持续采集;勾选后采样循环才读取频率并绘制到右侧频率曲线" />
|
||||||
|
<TextBlock Text="频率" Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Style="{StaticResource MeasureBox}"
|
||||||
|
Text="{Binding FrequencyNow, Mode=OneWay, StringFormat={}{0:F2}}" />
|
||||||
|
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,12,0" />
|
||||||
|
<TextBlock Text="功率" Style="{StaticResource ParamLabel}" />
|
||||||
|
<TextBox Style="{StaticResource MeasureBox}"
|
||||||
|
Text="{Binding PowerNow, Mode=OneWay, StringFormat={}{0:F2}}" />
|
||||||
|
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 日志 -->
|
||||||
|
<GroupBox Header="日志" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||||
|
<ScrollViewer Height="150" 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>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- ============ 右侧:三相电压 + 输出频率(横轴为采集经过时间 s) ============ -->
|
||||||
|
<Grid Grid.Column="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 三相电压 -->
|
||||||
|
<Border Grid.Row="0" Background="White" BorderBrush="#DDD" BorderThickness="1" Margin="0,0,0,4">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="10,6">
|
||||||
|
<TextBlock Text="三相电压 (V)" FontWeight="Bold" FontSize="13"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,16,0" />
|
||||||
|
<TextBlock Text="A" FontSize="12" Foreground="#E53935" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding VoltageA, StringFormat={}{0:F1}}" FontSize="12"
|
||||||
|
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
|
||||||
|
<TextBlock Text="B" FontSize="12" Foreground="#43A047" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding VoltageB, StringFormat={}{0:F1}}" FontSize="12"
|
||||||
|
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
|
||||||
|
<TextBlock Text="C" FontSize="12" Foreground="#1E88E5" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding VoltageC, StringFormat={}{0:F1}}" FontSize="12"
|
||||||
|
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
<oxy:PlotView Grid.Row="1" Model="{Binding VoltagePlot}"
|
||||||
|
Background="Transparent" Margin="4,0,4,4" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 输出频率(受左侧“采集频率”开关控制,一般不采集) -->
|
||||||
|
<Border Grid.Row="1" Background="White" BorderBrush="#DDD" BorderThickness="1">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="10,6">
|
||||||
|
<TextBlock Text="输出频率 (Hz)" FontWeight="Bold" FontSize="13"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,16,0" />
|
||||||
|
<TextBlock Text="{Binding FrequencyNow, StringFormat={}{0:F2}}" FontSize="12"
|
||||||
|
FontWeight="Bold" Foreground="#8E24AA" VerticalAlignment="Center" Margin="3,0,14,0" />
|
||||||
|
<TextBlock FontSize="11" Foreground="#4CAF50" VerticalAlignment="Center">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="频率采集中" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsFrequencySamplingEnabled}" Value="False">
|
||||||
|
<Setter Property="Text" Value="频率采集未开启(在左侧“数据采集”勾选后才会绘制)" />
|
||||||
|
<Setter Property="Foreground" Value="#9E9E9E" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<oxy:PlotView Grid.Row="1" Model="{Binding FrequencyPlot}"
|
||||||
|
Background="Transparent" Margin="4,0,4,4" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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 PowerAdaptabilityModule.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PowerAdaptabilityView.xaml 的交互逻辑
|
||||||
|
/// </summary>
|
||||||
|
public partial class PowerAdaptabilityView : UserControl
|
||||||
|
{
|
||||||
|
public PowerAdaptabilityView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ namespace SettingModule
|
|||||||
|
|
||||||
// 设备连接配置弹窗
|
// 设备连接配置弹窗
|
||||||
containerRegistry.RegisterDialog<TCPConfigView>("TCPConfig");
|
containerRegistry.RegisterDialog<TCPConfigView>("TCPConfig");
|
||||||
|
containerRegistry.RegisterDialog<VisaConfigView>("VisaConfig");
|
||||||
containerRegistry.RegisterDialog<SerialPortConfigView>("SerialPortConfig");
|
containerRegistry.RegisterDialog<SerialPortConfigView>("SerialPortConfig");
|
||||||
containerRegistry.RegisterDialog<CANConfigView>("CANConfig");
|
containerRegistry.RegisterDialog<CANConfigView>("CANConfig");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using UIShare.PubEvent;
|
||||||
|
using UIShare.UIViewModel;
|
||||||
|
using UIShare.ViewModelBase;
|
||||||
|
|
||||||
|
|
||||||
|
namespace SettingModule.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VISA 连接配置对话框 VM。
|
||||||
|
/// 通过 DialogParameters 接收宿主 DeviceInfoVM;保存时把副本写回宿主。
|
||||||
|
/// </summary>
|
||||||
|
public class VisaConfigViewModel : DialogViewModelBase
|
||||||
|
{
|
||||||
|
#region 属性
|
||||||
|
|
||||||
|
private string _title = "VISA 连接配置";
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get => _title;
|
||||||
|
set => SetProperty(ref _title, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>编辑用的副本,取消时不会污染宿主对象。</summary>
|
||||||
|
private VisaConfigVM _config = new();
|
||||||
|
public VisaConfigVM Config
|
||||||
|
{
|
||||||
|
get => _config;
|
||||||
|
set => SetProperty(ref _config, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>常用超时(毫秒)。</summary>
|
||||||
|
public ObservableCollection<int> CommonTimeouts { get; } = new()
|
||||||
|
{
|
||||||
|
500, 1000, 2000, 3000, 5000, 10000
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>常见 VISA 资源字符串模板,便于快速填写(可直接编辑)。</summary>
|
||||||
|
public ObservableCollection<string> ResourceTemplates { get; } = new()
|
||||||
|
{
|
||||||
|
"TCPIP0::192.168.20.125::inst0::INSTR",
|
||||||
|
"TCPIP0::192.168.20.125::5025::SOCKET",
|
||||||
|
"USB0::0x0699::0x0408::C012345::INSTR",
|
||||||
|
"GPIB0::7::INSTR",
|
||||||
|
"ASRL1::INSTR"
|
||||||
|
};
|
||||||
|
|
||||||
|
private string _errorMessage = string.Empty;
|
||||||
|
public string ErrorMessage
|
||||||
|
{
|
||||||
|
get => _errorMessage;
|
||||||
|
set => SetProperty(ref _errorMessage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 命令
|
||||||
|
public ICommand SaveCommand { get; }
|
||||||
|
public ICommand CancelCommand { get; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// 用于保存时把副本回写到原对象
|
||||||
|
private DeviceInfoVM? _hostDevice;
|
||||||
|
|
||||||
|
public VisaConfigViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||||
|
{
|
||||||
|
SaveCommand = new DelegateCommand(OnSave);
|
||||||
|
CancelCommand = new DelegateCommand(OnCancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Validate(out string error)
|
||||||
|
{
|
||||||
|
error = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(Config.ResourceName))
|
||||||
|
{
|
||||||
|
error = "VISA 资源字符串不能为空";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Config.ResourceName.Contains("::"))
|
||||||
|
{
|
||||||
|
error = "资源字符串格式不正确,应形如 TCPIP0::192.168.0.10::inst0::INSTR";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Config.OpenTimeout < 0 || Config.IOTimeout < 0)
|
||||||
|
{
|
||||||
|
error = "超时时间不能为负数";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSave()
|
||||||
|
{
|
||||||
|
if (!Validate(out var error))
|
||||||
|
{
|
||||||
|
ErrorMessage = error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ErrorMessage = string.Empty;
|
||||||
|
|
||||||
|
// 把副本写回宿主
|
||||||
|
if (_hostDevice != null)
|
||||||
|
{
|
||||||
|
_hostDevice.VisaConfig ??= new VisaConfigVM();
|
||||||
|
Config.CopyTo(_hostDevice.VisaConfig);
|
||||||
|
_hostDevice.ConnectionType = "Visa";
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestClose.Invoke(ButtonResult.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel() => RequestClose.Invoke(ButtonResult.Cancel);
|
||||||
|
|
||||||
|
#region Prism Dialog 规范
|
||||||
|
public override void OnDialogOpened(IDialogParameters parameters)
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<OverlayEvent>().Publish(true);
|
||||||
|
|
||||||
|
if (parameters.ContainsKey("Device"))
|
||||||
|
{
|
||||||
|
_hostDevice = parameters.GetValue<DeviceInfoVM>("Device");
|
||||||
|
Title = $"VISA 连接配置 - {_hostDevice?.DeviceName}";
|
||||||
|
Config = new VisaConfigVM(_hostDevice?.VisaConfig);
|
||||||
|
}
|
||||||
|
else if (parameters.ContainsKey("Config"))
|
||||||
|
{
|
||||||
|
Config = new VisaConfigVM(parameters.GetValue<VisaConfigVM>("Config"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnDialogClosed()
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ namespace SettingModule.ViewModels
|
|||||||
|
|
||||||
public ObservableCollection<string> ConnectionTypes { get; } = new()
|
public ObservableCollection<string> ConnectionTypes { get; } = new()
|
||||||
{
|
{
|
||||||
"None", "Tcp", "Serial", "CAN"
|
"None", "Tcp", "Visa", "Serial", "CAN"
|
||||||
};
|
};
|
||||||
#endregion
|
#endregion
|
||||||
#region 命令
|
#region 命令
|
||||||
@@ -163,6 +163,7 @@ namespace SettingModule.ViewModels
|
|||||||
var dialogName = SelectedDevice.ConnectionType switch
|
var dialogName = SelectedDevice.ConnectionType switch
|
||||||
{
|
{
|
||||||
"Tcp" => "TCPConfig",
|
"Tcp" => "TCPConfig",
|
||||||
|
"Visa" => "VisaConfig",
|
||||||
"Serial" => "SerialPortConfig",
|
"Serial" => "SerialPortConfig",
|
||||||
"CAN" => "CANConfig",
|
"CAN" => "CANConfig",
|
||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<UserControl x:Class="SettingModule.Views.Dialogs.VisaConfigView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:SettingModule.Views.Dialogs"
|
||||||
|
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||||
|
xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare"
|
||||||
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
|
Background="White"
|
||||||
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
|
Width="500"
|
||||||
|
Height="300">
|
||||||
|
<prism:Dialog.WindowStyle>
|
||||||
|
<Style BasedOn="{StaticResource DialogUserManageStyle}"
|
||||||
|
TargetType="Window" />
|
||||||
|
</prism:Dialog.WindowStyle>
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:StringToVisibilityConverter x:Key="StringToVisibility"/>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<GroupBox Padding="12,8,12,8"
|
||||||
|
helpers:WindowDragHelper.EnableWindowDrag="True">
|
||||||
|
<GroupBox.Header>
|
||||||
|
<Grid Margin="0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
Foreground="White"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="5,0,10,0" />
|
||||||
|
</Grid>
|
||||||
|
</GroupBox.Header>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 表单 -->
|
||||||
|
<Grid Grid.Row="0" Margin="0,4,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="110"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
|
Text="资源字符串:"
|
||||||
|
VerticalAlignment="Center" Margin="0,6"/>
|
||||||
|
<ComboBox Grid.Row="0" Grid.Column="1" Margin="0,6"
|
||||||
|
IsEditable="True"
|
||||||
|
materialDesign:HintAssist.Hint="TCPIP0::IP::inst0::INSTR"
|
||||||
|
ItemsSource="{Binding ResourceTemplates}"
|
||||||
|
Text="{Binding Config.ResourceName, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||||
|
Text="打开超时(ms):"
|
||||||
|
VerticalAlignment="Center" Margin="0,6"/>
|
||||||
|
<ComboBox Grid.Row="1" Grid.Column="1" Margin="0,6"
|
||||||
|
IsEditable="True"
|
||||||
|
materialDesign:HintAssist.Hint=""
|
||||||
|
ItemsSource="{Binding CommonTimeouts}"
|
||||||
|
Text="{Binding Config.OpenTimeout, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||||
|
Text="读写超时(ms):"
|
||||||
|
VerticalAlignment="Center" Margin="0,6"/>
|
||||||
|
<ComboBox Grid.Row="2" Grid.Column="1" Margin="0,6"
|
||||||
|
IsEditable="True"
|
||||||
|
materialDesign:HintAssist.Hint=""
|
||||||
|
ItemsSource="{Binding CommonTimeouts}"
|
||||||
|
Text="{Binding Config.IOTimeout, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Foreground="#D32F2F"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="{Binding ErrorMessage}"
|
||||||
|
Visibility="{Binding ErrorMessage, Converter={StaticResource StringToVisibility}}"/>
|
||||||
|
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<StackPanel Grid.Row="2"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,12,0,0">
|
||||||
|
<Button Content="取消"
|
||||||
|
Width="80" Padding="0,4"
|
||||||
|
Command="{Binding CancelCommand}"/>
|
||||||
|
<Button Content="保存"
|
||||||
|
Width="80" Padding="0,4"
|
||||||
|
Margin="10,0,0,0"
|
||||||
|
IsDefault="True"
|
||||||
|
Command="{Binding SaveCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SettingModule.Views.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VisaConfigView.xaml 的交互逻辑
|
||||||
|
/// </summary>
|
||||||
|
public partial class VisaConfigView : UserControl
|
||||||
|
{
|
||||||
|
public VisaConfigView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,6 +124,11 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
alive = await ProbeTcpDeviceAsync(tcpDevice);
|
alive = await ProbeTcpDeviceAsync(tcpDevice);
|
||||||
}
|
}
|
||||||
|
// VISA 设备同理:IsConnected 仅表示会话对象存在,主动探活以检测死会话
|
||||||
|
else if (alive && device is Visa visaDevice)
|
||||||
|
{
|
||||||
|
alive = await ProbeVisaDeviceAsync(visaDevice);
|
||||||
|
}
|
||||||
|
|
||||||
if (alive)
|
if (alive)
|
||||||
{
|
{
|
||||||
@@ -176,6 +181,26 @@ namespace UIShare.GlobalVariable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对 VISA 设备执行轻量级主动探活(SCPI *IDN?)。
|
||||||
|
/// <para>通过 WriteReadAsync 内部获取 _commLock,与监控采样天然串行化;
|
||||||
|
/// 死会话将在 VISA 自身的 IOTimeout 内抛出异常,返回 false 触发重连。</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true: 探活成功(会话确实存活); false: 探活失败(死会话)</returns>
|
||||||
|
private async Task<bool> ProbeVisaDeviceAsync(Visa visaDevice)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var probeCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||||
|
string resp = await visaDevice.WriteReadAsync("*IDN?\n", "\n", probeCts.Token);
|
||||||
|
return !string.IsNullOrWhiteSpace(resp);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重连单个设备。
|
/// 重连单个设备。
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ namespace UIShare.GlobalVariable
|
|||||||
return $"Tcp:{config.TcpConfig.IPAddress}:{config.TcpConfig.Port}";
|
return $"Tcp:{config.TcpConfig.IPAddress}:{config.TcpConfig.Port}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(config.ConnectionType, "Visa", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& config.VisaConfig != null)
|
||||||
|
{
|
||||||
|
return $"Visa:{config.VisaConfig.ResourceName}";
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(config.ConnectionType, "Serial", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(config.ConnectionType, "Serial", StringComparison.OrdinalIgnoreCase)
|
||||||
&& config.SerialPortConfig != null)
|
&& config.SerialPortConfig != null)
|
||||||
{
|
{
|
||||||
@@ -129,6 +135,7 @@ namespace UIShare.GlobalVariable
|
|||||||
return config.ConnectionType switch
|
return config.ConnectionType switch
|
||||||
{
|
{
|
||||||
"Tcp" => CreateTcpDevice(deviceType, config.TcpConfig)!,
|
"Tcp" => CreateTcpDevice(deviceType, config.TcpConfig)!,
|
||||||
|
"Visa" => CreateVisaDevice(deviceType, config.VisaConfig)!,
|
||||||
"Serial" => CreateSerialDevice(deviceType, config.SerialPortConfig)!,
|
"Serial" => CreateSerialDevice(deviceType, config.SerialPortConfig)!,
|
||||||
_ => null!
|
_ => null!
|
||||||
};
|
};
|
||||||
@@ -379,6 +386,7 @@ namespace UIShare.GlobalVariable
|
|||||||
bool ok = conn switch
|
bool ok = conn switch
|
||||||
{
|
{
|
||||||
"Tcp" => await ConnectTcpAsync(name, device, ct),
|
"Tcp" => await ConnectTcpAsync(name, device, ct),
|
||||||
|
"Visa" => await ConnectVisaAsync(name, device, ct),
|
||||||
"Serial" => await ConnectSerialAsync(name, device, ct),
|
"Serial" => await ConnectSerialAsync(name, device, ct),
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
@@ -417,6 +425,16 @@ namespace UIShare.GlobalVariable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> ConnectVisaAsync(string name, IBaseInterface device, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (device is IVisa visa)
|
||||||
|
{
|
||||||
|
return await visa.ConnectAsync(ct);
|
||||||
|
}
|
||||||
|
LoggerHelper.Warn($"设备 [{name}] 配置为 Visa 但未实现 IVisa,实际类型为 {device.GetType().Name}。");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<bool> ConnectSerialAsync(string name, IBaseInterface device, CancellationToken ct)
|
private static async Task<bool> ConnectSerialAsync(string name, IBaseInterface device, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (device is not ISerialPort sp)
|
if (device is not ISerialPort sp)
|
||||||
@@ -533,8 +551,35 @@ namespace UIShare.GlobalVariable
|
|||||||
SendTimeout = vm.SendTimeout,
|
SendTimeout = vm.SendTimeout,
|
||||||
ReceiveTimeout = vm.ReceiveTimeout
|
ReceiveTimeout = vm.ReceiveTimeout
|
||||||
};
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"实例化 Tcp 设备 [{type.Name}] 失败(连接配置与设备构造函数不匹配?):{ex.InnerException?.Message ?? ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static IBaseInterface? CreateVisaDevice(Type type, VisaConfigVM? vm)
|
||||||
|
{
|
||||||
|
vm ??= new VisaConfigVM();
|
||||||
|
var cfg = new VisaConfig
|
||||||
|
{
|
||||||
|
ResourceName = vm.ResourceName,
|
||||||
|
OpenTimeout = vm.OpenTimeout,
|
||||||
|
IOTimeout = vm.IOTimeout
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"实例化 Visa 设备 [{type.Name}] 失败(连接配置与设备构造函数不匹配?):{ex.InnerException?.Message ?? ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
private static IBaseInterface? CreateSerialDevice(Type type, SerialPortConfigVM? vm)
|
private static IBaseInterface? CreateSerialDevice(Type type, SerialPortConfigVM? vm)
|
||||||
{
|
{
|
||||||
vm ??= new SerialPortConfigVM();
|
vm ??= new SerialPortConfigVM();
|
||||||
@@ -548,8 +593,16 @@ namespace UIShare.GlobalVariable
|
|||||||
ReadTimeout = vm.ReadTimeout,
|
ReadTimeout = vm.ReadTimeout,
|
||||||
WriteTimeout = vm.WriteTimeout
|
WriteTimeout = vm.WriteTimeout
|
||||||
};
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"实例化 Serial 设备 [{type.Name}] 失败(连接配置与设备构造函数不匹配?):{ex.InnerException?.Message ?? ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
||||||
/// 委托签名统一为 Func<object, CancellationToken, Task<string>>
|
/// 委托签名统一为 Func<object, CancellationToken, Task<double>>
|
||||||
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
||||||
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
||||||
/// - 返回值 = Task<string>
|
/// - 返回值 = Task<double>(Task<string> 方法自动 TryParse 为 double)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<string>> Invoker, object Device)> _registeredMethods = new();
|
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<double>> Invoker, object Device)> _registeredMethods = new();
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -76,11 +76,13 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
||||||
/// 统一签名为 Func<object, CancellationToken, Task<string>>:
|
/// 统一签名为 Func<object, CancellationToken, Task<double>>:
|
||||||
|
/// - Task<double> 方法:直接返回 double 值
|
||||||
|
/// - Task<string> 方法:自动 TryParse 为 double
|
||||||
/// - 无参方法:忽略 CancellationToken
|
/// - 无参方法:忽略 CancellationToken
|
||||||
/// - 带 CancellationToken 方法:直接透传
|
/// - 带 CancellationToken 方法:直接透传
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static Func<object, CancellationToken, Task<string>> BuildInvoker(MethodInfo method)
|
private static Func<object, CancellationToken, Task<double>> BuildInvoker(MethodInfo method)
|
||||||
{
|
{
|
||||||
var deviceParam = Expression.Parameter(typeof(object), "device");
|
var deviceParam = Expression.Parameter(typeof(object), "device");
|
||||||
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
||||||
@@ -96,9 +98,25 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
var call = Expression.Call(castDevice, method, callArgs);
|
var call = Expression.Call(castDevice, method, callArgs);
|
||||||
|
|
||||||
return Expression.Lambda<Func<object, CancellationToken, Task<string>>>(
|
if (method.ReturnType == typeof(Task<double>))
|
||||||
|
{
|
||||||
|
// Task<double> 方法:直接返回
|
||||||
|
return Expression.Lambda<Func<object, CancellationToken, Task<double>>>(
|
||||||
call, deviceParam, ctParam).Compile();
|
call, deviceParam, ctParam).Compile();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Task<string> 方法:await 后 TryParse 为 double
|
||||||
|
return async (device, ct) =>
|
||||||
|
{
|
||||||
|
// 通过 MethodInfo.Invoke 调用(仅 Task<string> 路径,性能可接受)
|
||||||
|
var task = (Task<string>)method.Invoke(device,
|
||||||
|
parms.Length == 1 ? new object[] { ct } : Array.Empty<object>())!;
|
||||||
|
string raw = await task.ConfigureAwait(false);
|
||||||
|
return double.TryParse(raw, out double v) ? v : double.NaN;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
||||||
@@ -126,7 +144,9 @@ namespace UIShare.GlobalVariable
|
|||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
// 同时支持 Task<string> 和 Task<double> 返回类型
|
||||||
|
var rt = m.ReturnType;
|
||||||
|
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
return parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
@@ -184,10 +204,10 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 直接委托调用,零反射开销
|
// 直接委托调用,零反射开销(Task<double> 路径)或自动 TryParse(Task<string> 路径)
|
||||||
string raw = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
double value = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!double.TryParse(raw, out double value)) return;
|
if (double.IsNaN(value)) return;
|
||||||
|
|
||||||
// 采样成功,重置失败计数
|
// 采样成功,重置失败计数
|
||||||
_failureCounts.TryRemove(channelKey, out _);
|
_failureCounts.TryRemove(channelKey, out _);
|
||||||
|
|||||||
@@ -61,6 +61,14 @@ namespace UIShare.UIViewModel
|
|||||||
set => SetProperty(ref _tcpConfig, value);
|
set => SetProperty(ref _tcpConfig, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>VISA 连接参数(首次访问时自动初始化,便于 XAML 直接绑定)。</summary>
|
||||||
|
private VisaConfigVM _visaConfig = new();
|
||||||
|
public VisaConfigVM VisaConfig
|
||||||
|
{
|
||||||
|
get => _visaConfig;
|
||||||
|
set => SetProperty(ref _visaConfig, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>串口连接参数(首次访问时自动初始化,便于 XAML 直接绑定)。</summary>
|
/// <summary>串口连接参数(首次访问时自动初始化,便于 XAML 直接绑定)。</summary>
|
||||||
private SerialPortConfigVM _serialPortConfig = new();
|
private SerialPortConfigVM _serialPortConfig = new();
|
||||||
public SerialPortConfigVM SerialPortConfig
|
public SerialPortConfigVM SerialPortConfig
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Prism.Mvvm;
|
||||||
|
|
||||||
|
namespace UIShare.UIViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VISA 连接配置(与 DeviceCommand.Base.Visa 保持字段一致)。
|
||||||
|
/// <para>VISA 通过统一资源字符串寻址,无需区分 IP/端口,例如
|
||||||
|
/// TCPIP0::192.168.20.125::inst0::INSTR、USB0::0x0699::0x0408::C012345::INSTR。</para>
|
||||||
|
/// </summary>
|
||||||
|
public class VisaConfigVM : BindableBase
|
||||||
|
{
|
||||||
|
private string _resourceName = "TCPIP0::127.0.0.1::inst0::INSTR";
|
||||||
|
/// <summary>VISA 资源字符串(TCPIP / USB / GPIB / ASRL 等统一寻址)。</summary>
|
||||||
|
public string ResourceName
|
||||||
|
{
|
||||||
|
get => _resourceName;
|
||||||
|
set => SetProperty(ref _resourceName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _openTimeout = 3000;
|
||||||
|
/// <summary>打开会话超时(毫秒)。</summary>
|
||||||
|
public int OpenTimeout
|
||||||
|
{
|
||||||
|
get => _openTimeout;
|
||||||
|
set => SetProperty(ref _openTimeout, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _ioTimeout = 3000;
|
||||||
|
/// <summary>读写 I/O 超时(毫秒)。</summary>
|
||||||
|
public int IOTimeout
|
||||||
|
{
|
||||||
|
get => _ioTimeout;
|
||||||
|
set => SetProperty(ref _ioTimeout, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VisaConfigVM() { }
|
||||||
|
|
||||||
|
/// <summary>拷贝构造,用于对话框编辑副本。</summary>
|
||||||
|
public VisaConfigVM(VisaConfigVM? src)
|
||||||
|
{
|
||||||
|
if (src == null) return;
|
||||||
|
ResourceName = src.ResourceName;
|
||||||
|
OpenTimeout = src.OpenTimeout;
|
||||||
|
IOTimeout = src.IOTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>把字段拷回目标对象(保存时用)。</summary>
|
||||||
|
public void CopyTo(VisaConfigVM? dst)
|
||||||
|
{
|
||||||
|
if (dst == null) return;
|
||||||
|
dst.ResourceName = ResourceName;
|
||||||
|
dst.OpenTimeout = OpenTimeout;
|
||||||
|
dst.IOTimeout = IOTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user