using Model.Models; using NationalInstruments.Visa; using System; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DeviceCommand.Base { /// /// VISA SCPI 通信基类:基于 NI-VISA(NationalInstruments.Visa)实现, /// 对外暴露与 完全一致的异步 API(SendAsync / ReadAsync / WriteReadAsync / ReadAllBytesAsync), /// 使 SCPI 设备类(如 TektronixMSO)能在 Tcp 与 VISA 传输之间平滑切换。 /// VISA 通过统一的资源字符串寻址(TCPIP0::IP::inst0::INSTR、USB0::...、GPIB0::...::INSTR 等), /// 由 NI-VISA 运行时自动选择对应传输层;本类只面向消息型会话 。 /// 注意:VISA 的读写为同步阻塞调用,本基类统一通过 投递到线程池, /// 以保持与 Tcp 一致的 async/await 契约、避免阻塞 UI 线程。 /// public class Visa : IVisa, IDisposable { /// VISA 资源字符串(例如 TCPIP0::192.168.20.125::inst0::INSTR)。 public string ResourceName { get; set; } = "TCPIP0::127.0.0.1::inst0::INSTR"; /// 打开 VISA 会话的最长等待时间(毫秒)。 public int OpenTimeout { get; set; } = 3000; /// 读写 I/O 超时(毫秒),对应 VISA 的 TimeoutMilliseconds。 public int IOTimeout { get; set; } = 3000; /// 底层 VISA 消息型会话;为 null 表示未连接。 private MessageBasedSession? _session; /// 通信互斥锁:VISA 会话不支持并发读写,所有 I/O 串行化。 protected readonly SemaphoreSlim _commLock = new(1, 1); public bool IsConnected => _session != null; public Visa() { } /// /// 通过 一次性配置 VISA 通信参数。 /// 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 ConnectAsync(CancellationToken ct = default) { await _commLock.WaitAsync(ct); try { return await Task.Run(() => ResetConnection(), ct); } finally { _commLock.Release(); } } /// /// 核心内部方法:释放旧会话并按资源字符串重建 VISA 会话。 /// 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; } /// /// 按 VISA 资源字符串前缀创建对应的具体会话实例;创建失败时返回 null(不抛异常)。 /// NI-VISA .NET 的基类 构造函数为 protected,无法直接实例化, /// 必须使用其 public 构造函数的具体子类(TcpipSession/GpibSession/UsbSession/SerialSession), /// 它们均派生自 ,故可统一以基类引用持有。 /// 资源字符串为空、类型无法识别或底层打开会话失败(如示波器未连接)时一律返回 null, /// 由 按“未连接”处理,避免设备缺省时抛异常中断上层初始化。 /// 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(); } /// 线程安全地释放当前会话并置空(幂等)。 private void DisposeSession() { var s = Interlocked.Exchange(ref _session, null); try { s?.Dispose(); } catch { /* 释放阶段忽略底层异常 */ } } /// 确保会话已建立,否则抛出与 Tcp 一致语义的异常。 private void EnsureConnected() { if (_session == null) throw new InvalidOperationException("VISA 会话未连接。"); } /// 按分隔符设置读取终止字符(取分隔符最后一个字符,兼容 "\r\n")。 private void ApplyTermination(string? delimiter) { if (string.IsNullOrEmpty(delimiter)) return; _session!.TerminationCharacter = (byte)delimiter[delimiter.Length - 1]; _session!.TerminationCharacterEnabled = true; } /// /// 宽松解码设备回传的字节为字符串。 /// NI-VISA 的 RawIO.ReadString() 采用严格解码,遇到非文本/高位字节会抛 /// ;而 SCPI 文本响应本质是 ASCII, /// 故改用 RawIO.Read() 取原始字节再以 Encoding.ASCII 解码—— /// 非法字节被替换为 '?',绝不抛异常。 /// 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(); } } /// /// 读取定长原始二进制字节(用于 IT6720 等自定义二进制帧协议)。 /// VISA 会话在 中默认启用了以 \n(0x0A) 为终止符的文本读取; /// 而二进制帧的数据区可能恰好包含 0x0A 字节,若不禁用终止符,RawIO.Read 会在该字节处提前结束, /// 导致返回长度不足、帧校验失败。故本方法读取期间临时关闭终止符,读完再由 finally 恢复。 /// 单次 RawIO.Read(count) 只保证返回「至多 count」字节,可能因分包而少于请求长度; /// 这里参照 的做法循环补足,直到读满 length 或发生超时/END/流终止。 /// public async Task ReadAsync(int length, CancellationToken ct = default) { await _commLock.WaitAsync(ct); try { EnsureConnected(); return await Task.Run(() => ReadExactBytes(length), ct); } finally { _commLock.Release(); } } /// /// 同步读取定长二进制字节:关闭终止符后循环补足,直到读满 字节; /// 超时/END/流终止导致读不满时按实到长度截断返回(由上层按帧长校验判定成功与否)。 /// private byte[] ReadExactBytes(int length) { if (length <= 0) return Array.Empty(); bool prevTermEnabled = _session!.TerminationCharacterEnabled; // 二进制帧数据可能含 0x0A,读取全程禁用终止符,避免被误判为帧结束而截断 _session!.TerminationCharacterEnabled = false; try { var buffer = new byte[length]; int offset = 0; while (offset < length) { byte[] chunk; try { chunk = _session!.RawIO.Read(length - offset); } catch (Exception) when (offset > 0) { // 已收到部分数据后再超时/异常:返回已收字节,交由上层按帧长校验处理 break; } if (chunk == null || chunk.Length == 0) break; Buffer.BlockCopy(chunk, 0, buffer, offset, chunk.Length); offset += chunk.Length; } if (offset == length) return buffer; // 实际读到的字节少于请求长度:按实到长度截断返回,避免尾部填充 0 var trimmed = new byte[offset]; Buffer.BlockCopy(buffer, 0, trimmed, 0, offset); return trimmed; } finally { _session!.TerminationCharacterEnabled = prevTermEnabled; } } public async Task 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 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(); } } /// /// 发送查询命令并读取设备回传的 IEEE 488.2 定长二进制块,自动剥离块头(#N<长度>),只返回纯数据字节。 /// 示波器 FILESystem:READFile 回传的 PNG 截图、CURVe? 波形等均以该块格式封装: /// #9<9位长度><二进制数据><LF>。若把含 #9... 头的整段原始字节直接写入 .png, /// 文件将不以 PNG 魔数(\x89PNG)开头而被判为损坏——故必须解析并丢弃块头,只落地纯数据。 /// 读取期间关闭终止符,避免数据中的 0x0A 被误判为结束;并按块头声明的长度精确读取, /// 不再依赖 END 指示,从根本上规避大块传输的 IOTimeoutException。 /// public async Task ReadAllBytesAsync(string queryCommand, CancellationToken ct = default) { await _commLock.WaitAsync(ct); try { EnsureConnected(); return await Task.Run(() => ReadIEEEBlock(queryCommand), ct); } finally { _commLock.Release(); } } /// /// 同步读取一个 IEEE 488.2 定长二进制块(#N<长度><数据><终止符>)并返回其纯数据部分。 /// 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:启用终止符,读到换行为止 _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(); // 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; } } /// 读取 1 个字节并返回其整数值(0-255);无数据则抛异常。 private int ReadOneByte() { byte[] b = _session!.RawIO.Read(1); if (b == null || b.Length == 0) throw new InvalidOperationException("读取 IEEE 块头时未收到任何字节。"); return b[0]; } /// 跳过前导空白/换行后读取块头起始字节,返回其整数值。 private int ReadBlockStartByte() { int b = ReadOneByte(); for (int guard = 0; (b == '\n' || b == '\r' || b == ' ' || b == '\t') && guard < 64; guard++) b = ReadOneByte(); return b; } /// 把首字节并回数组前部(用于设备直接回裸数据的退化路径)。 private static byte[] PrependByte(byte first, byte[]? rest) { rest ??= Array.Empty(); var result = new byte[rest.Length + 1]; result[0] = first; Buffer.BlockCopy(rest, 0, result, 1, rest.Length); return result; } /// /// 循环读取设备回传的裸二进制数据,直到收到 END 指示或终止符为止(参考 ADP TCP “读到没有为止” 策略)。 /// 无参 RawIO.Read() 只返回首个内部缓冲(约 1KB)即结束,会把大文件(如 PNG 截图)截断; /// 本方法改用 RawIO.Read(count, out ReadStatus) 分块读取,依据 判定结束: /// EndReceived / TerminationCharacterEncountered 表示传输完成, /// MaximumCountReached 表示本次读满、可能仍有数据需继续读取。 /// /// 调用前的 I/O 超时(毫秒);大块传输期间在此放宽,读取结束后由调用方 finally 统一恢复。 /// 设备回传的完整裸二进制字节。 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); } } }