示波器修改:visa连接方式+截图保存
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user