# IT8702P 电子负载驱动接入文档(C# / WPF / .NET 8) > 本文档供 AI 开发助手作为实现依据使用。目标:在 C# + WPF + .NET 8 主项目中, > 通过 LAN Socket 接入 ITECH IT8702P 电子负载,采集**电压 / 电流 / 功率**三项数据, > 既能实时绑定到界面显示,也能由后台连续记录。 > > **AI 开发约束**:严格遵循本文协议与命令格式。不要臆造未在本文出现的 SCPI 命令; > 命令一律以 `\n` 结尾;查询命令必须读完整行响应后再发下一条;设置类命令前必须已发送 `SYST:REM`。 --- ## 1. 通讯概览 | 项目 | 值 | |------|-----| | 物理接口 | LAN(以太网) | | 传输协议 | 标准 TCP(raw socket / SCPI-RAW),与普通 socket 编程完全一致 | | 默认 IP | `192.168.200.100`(以设备实际配置为准,需可在设置中修改) | | 端口 | `30000` | | 命令语言 | SCPI(ASCII 文本,大小写不敏感) | | 命令结束符 | `\n`(LF,0x0A) | | 响应结束符 | `\n`(按行读取) | | 编码 | ASCII | | 通道 | 主机带多负载模块;默认操作 `CH1`,可切换(`CHANnel <1-8>`) | 要点: - 不需要 VISA、不需要厂商驱动 / SDK,`System.Net.Sockets.TcpClient` 直接连接即可。 - **同步请求-应答模型**:每条查询命令发出后必须读回一行响应,才能发下一条;否则设备会报 `Query Interrupted`,且数据丢失。 - 设备内部一次测量约几 ms 到十几 ms,单点轮询实际刷新率为每秒数十次量级,**不适合高速波形采集**(高速场景由独立采集卡负责,不在本驱动范围内)。 --- ## 2. SCPI 命令清单(仅本项目所需) ### 2.1 连接与会话 | 命令 | 类型 | 说明 | |------|------|------| | `*IDN?` | 查询 | 返回厂商/型号/序列号/固件版本,逗号分隔。用于连接自检 | | `SYST:REM` | 设置 | 进入远程模式。**任何设置类命令前必须先发**。只读测量不强制,但建议连接后即发 | | `SYST:LOC` | 设置 | 返回本地模式(前面板控制)。断开会话时可选发送 | | `SYST:ERR?` | 查询 | 读取错误队列下一条,格式 `,`,如 `0,"No Error"`。无错时返回 `0,No Error` | | `*CLS` | 设置 | 清除状态/错误队列 | ### 2.2 通道选择(多模块寻址) | 命令 | 类型 | 说明 | |------|------|------| | `CHAN ` | 设置 | 选择当前操作通道,`n` 取 1–8。默认 `CHAN 1`。后续命令作用于该通道 | | `CHAN?` | 查询 | 返回当前选中通道号 `` | | `CHAN:ID?` | 查询 | 返回当前通道模块的型号/序列号/固件,逗号分隔 | > 单通道使用时,连接后发一次 `CHAN 1` 即可;切换通道时重发 `CHAN `。 ### 2.3 测量(核心:电压 / 电流 / 功率) | 命令 | 类型 | 返回 | 说明 | |------|------|------|------| | `MEAS:VOLT?` | 查询 | `` | 触发一次新测量并返回电压(V)。较慢 | | `MEAS:CURR?` | 查询 | `` | 触发一次新测量并返回电流(A) | | `MEAS:POW?` | 查询 | `` | 触发一次新测量并返回功率(W) | | `FETC:VOLT?` | 查询 | `` | 取最近一次采样的电压,不重新测量。较快 | | `FETC:CURR?` | 查询 | `` | 取最近采样电流 | | `FETC:POW?` | 查询 | `` | 取最近采样功率 | 返回值为 ASCII 科学计数字符串,如 `2.971700E+00`,用 `double.Parse(s, CultureInfo.InvariantCulture)` 解析。 > **`MEAS` vs `FETC`**:`MEAS` 每次强制重新测量,数值最新但慢;`FETC` 取设备已有采样,快但可能略滞后。连续轮询场景推荐 `FETC` 以提升刷新率。 ### 2.4 合并查询(一次往返取三个值,强烈推荐) SCPI 允许用分号在一条消息里串多个查询,**一次往返**拿回多值,往返次数从 3 次降到 1 次: ```text MEAS:VOLT?;:MEAS:CURR?;:MEAS:POW? ``` - 第二条及之后每条查询前加 `:`,表示回到命令根路径(否则会拼接出错误路径)。 - 响应为分号分隔的一行,如 `2.971700E+00;1.040000E+00;3.090000E+00`,按 `;` 拆分解析。 - `FETC` 同理可合并:`FETC:VOLT?;:FETC:CURR?;:FETC:POW?` --- ## 3. 标准交互流程 ```text [连接] TcpClient.Connect(ip, 30000) 设置 TcpClient.NoDelay = true // 关闭 Nagle,消除小包延迟 发送 *IDN? → 读回一行,校验包含 "IT87" 字样,确认设备正确 发送 SYST:REM 发送 CHAN 1 [采集循环](按设定周期,如每 50~200ms 一次) 发送 FETC:VOLT?;:FETC:CURR?;:FETC:POW? 读回一行 → 按 ';' 拆分 → 解析为 (V, I, P) 推送给 UI(绑定)与 后台记录器(队列) [断开] 发送 SYST:LOC(可选) 关闭 socket ``` 错误处理约定: - 每条查询发送后若在超时内(建议 1–3 s)未读到完整行 → 视为通讯异常,触发重连流程。 - 周期性(或异常后)发送 `SYST:ERR?` 检查设备侧错误队列,非 `0,No Error` 则记录日志。 - 读响应以 `\n` 为行边界;TCP 可能分包,需循环 `Read` 直到读到 `\n`。 --- ## 4. C# 实现指引(.NET 8) ### 4.1 分层结构建议 ``` Driver 层 It8702pClient —— 纯通讯:连接、收发、按行读取、命令封装 采集层 AcquisitionService —— 后台采集循环(Task + PeriodicTimer),产出读数 分发层 读数 → ① UI 绑定(INotifyPropertyChanged,Dispatcher 回主线程) → ② 记录器(Channel 写入文件/数据库,独立消费者) ViewModel 层 绑定 Voltage / Current / Power 与 IsConnected 等状态 ``` 设计要点(对应"实时绑定 + 后台记录两者都要"): - **采集只跑一遍**,结果同时分发给 UI 和记录器,避免对设备发起两套独立轮询(同步协议下并发查询会互相打断)。 - UI 更新必须切回 UI 线程:`Application.Current.Dispatcher.Invoke(...)` 或在采集 Task 中用 `IProgress`。 - 后台记录用 `System.Threading.Channels.Channel`:采集线程只管入队(非阻塞),单独的写盘消费者出队,互不阻塞,避免磁盘 IO 拖慢采集节奏。 - 所有 socket 收发集中在 Driver 层,**全程串行**(同一时刻只有一条命令在途)。如有多处调用,用 `SemaphoreSlim(1,1)` 串行化,杜绝 `Query Interrupted`。 ### 4.2 数据模型 ```csharp public readonly record struct Reading( DateTime TimestampUtc, int Channel, double Voltage, // V double Current, // A double Power); // W ``` ### 4.3 Driver 层参考实现 ```csharp using System.Globalization; using System.Net.Sockets; using System.Text; public sealed class It8702pClient : IDisposable { private readonly SemaphoreSlim _gate = new(1, 1); private TcpClient? _tcp; private NetworkStream? _stream; public bool IsConnected => _tcp?.Connected ?? false; public async Task ConnectAsync(string ip, int port = 30000, int channel = 1, CancellationToken ct = default) { _tcp = new TcpClient { NoDelay = true }; // 关闭 Nagle await _tcp.ConnectAsync(ip, port, ct); _stream = _tcp.GetStream(); _stream.ReadTimeout = 3000; _stream.WriteTimeout = 3000; var idn = await QueryAsync("*IDN?", ct); if (!idn.Contains("IT87", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"意外的设备标识: {idn}"); await SendAsync("SYST:REM", ct); // 进入远程模式 await SendAsync($"CHAN {channel}", ct); // 选择通道 } /// 一次往返采集 V/I/P。 public async Task<(double V, double I, double P)> ReadVipAsync(CancellationToken ct = default) { // FETC 较快;如需每次强制重测可换成 MEAS var resp = await QueryAsync("FETC:VOLT?;:FETC:CURR?;:FETC:POW?", ct); var parts = resp.Split(';', StringSplitOptions.TrimEntries); if (parts.Length < 3) throw new FormatException($"响应格式异常: {resp}"); return (ParseNum(parts[0]), ParseNum(parts[1]), ParseNum(parts[2])); } public Task SelectChannelAsync(int channel, CancellationToken ct = default) => SendAsync($"CHAN {channel}", ct); public async Task ReadErrorAsync(CancellationToken ct = default) => await QueryAsync("SYST:ERR?", ct); // ---- 底层收发(全程串行)---- private async Task SendAsync(string cmd, CancellationToken ct) { await _gate.WaitAsync(ct); try { await WriteLineAsync(cmd, ct); } finally { _gate.Release(); } } private async Task QueryAsync(string cmd, CancellationToken ct) { await _gate.WaitAsync(ct); try { await WriteLineAsync(cmd, ct); return await ReadLineAsync(ct); } finally { _gate.Release(); } } private async Task WriteLineAsync(string cmd, CancellationToken ct) { var bytes = Encoding.ASCII.GetBytes(cmd + "\n"); await _stream!.WriteAsync(bytes, ct); } // TCP 可能分包,循环读到 '\n' 为止 private async Task ReadLineAsync(CancellationToken ct) { var sb = new StringBuilder(); var buf = new byte[1]; while (true) { int n = await _stream!.ReadAsync(buf, ct); if (n == 0) throw new IOException("连接已关闭"); char c = (char)buf[0]; if (c == '\n') break; if (c != '\r') sb.Append(c); } return sb.ToString().Trim(); } private static double ParseNum(string s) => double.Parse(s, NumberStyles.Float, CultureInfo.InvariantCulture); public void Dispose() { try { _stream?.Dispose(); _tcp?.Dispose(); } catch { /* 忽略 */ } _gate.Dispose(); } } ``` ### 4.4 采集服务参考实现 ```csharp using System.Threading.Channels; public sealed class AcquisitionService : IAsyncDisposable { private readonly It8702pClient _client; private readonly Channel _record = Channel.CreateUnbounded(); private CancellationTokenSource? _cts; private Task? _loop; public AcquisitionService(It8702pClient client) => _client = client; /// 供后台记录器消费的读数流。 public ChannelReader Records => _record.Reader; /// UI 通过此回调实时获取读数(在 ViewModel 中切回 UI 线程)。 public event Action? ReadingReceived; public void Start(int channel, TimeSpan period) { _cts = new CancellationTokenSource(); _loop = RunAsync(channel, period, _cts.Token); } private async Task RunAsync(int channel, TimeSpan period, CancellationToken ct) { using var timer = new PeriodicTimer(period); while (await timer.WaitForNextTickAsync(ct)) { try { var (v, i, p) = await _client.ReadVipAsync(ct); var reading = new Reading(DateTime.UtcNow, channel, v, i, p); ReadingReceived?.Invoke(reading); // → UI _record.Writer.TryWrite(reading); // → 记录器(非阻塞入队) } catch (OperationCanceledException) { break; } catch (Exception ex) { // TODO: 记录日志 / 触发重连 System.Diagnostics.Debug.WriteLine($"采集异常: {ex.Message}"); } } } public async ValueTask DisposeAsync() { _cts?.Cancel(); if (_loop is not null) try { await _loop; } catch { /* 忽略 */ } _record.Writer.TryComplete(); _cts?.Dispose(); } } ``` ### 4.5 ViewModel 绑定要点 ```csharp public sealed class MainViewModel : INotifyPropertyChanged { private double _voltage, _current, _power; public double Voltage { get => _voltage; private set => Set(ref _voltage, value); } public double Current { get => _current; private set => Set(ref _current, value); } public double Power { get => _power; private set => Set(ref _power, value); } // 订阅 AcquisitionService.ReadingReceived,并切回 UI 线程: private void OnReading(Reading r) => Application.Current.Dispatcher.Invoke(() => { Voltage = r.Voltage; Current = r.Current; Power = r.Power; }); // ... INotifyPropertyChanged / Set 实现略 public event PropertyChangedEventHandler? PropertyChanged; private void Set(ref T field, T value, [System.Runtime.CompilerServices.CallerMemberName] string? name = null) { field = value; PropertyChanged?.Invoke(this, new(name)); } } ``` 后台记录消费者(独立运行,写文件/数据库): ```csharp await foreach (var r in acquisition.Records.ReadAllAsync(ct)) { // 写 CSV / SQLite / 其他。IO 慢也不会阻塞采集循环 await writer.WriteLineAsync( $"{r.TimestampUtc:O},{r.Channel},{r.Voltage},{r.Current},{r.Power}"); } ``` --- ## 5. 配置项(建议放入主项目设置) | 配置 | 默认值 | 说明 | |------|--------|------| | `Ip` | `192.168.200.100` | 设备 IP,可改 | | `Port` | `30000` | 固定,一般不改 | | `Channel` | `1` | 操作通道,可切换 1–8 | | `PeriodMs` | `100` | 采集周期(毫秒)。过小无意义,受设备测量速度限制 | | `UseFetch` | `true` | `true` 用 `FETC`(快),`false` 用 `MEAS`(每次重测) | | `TimeoutMs` | `3000` | 收发超时 | --- ## 6. 易错点检查清单(AI 实现时务必核对) - [ ] 每条命令结尾加 `\n`。 - [ ] 设置类命令前已发送 `SYST:REM`。 - [ ] 查询发出后**读完整行响应再发下一条**;所有收发经 `SemaphoreSlim` 串行化。 - [ ] 合并查询时第二条起每条前加 `:`(回根路径)。 - [ ] 数字解析用 `CultureInfo.InvariantCulture`(避免逗号小数点区域问题)。 - [ ] 读响应循环到 `\n`,处理 TCP 分包。 - [ ] `TcpClient.NoDelay = true`。 - [ ] UI 更新切回 Dispatcher 线程。 - [ ] 采集只一套循环,结果分发给 UI 与记录器,不开两套独立轮询。 - [ ] 不要在本驱动里追求高速波形——高速由独立采集卡负责。 --- ## 7. 范围说明 本驱动只覆盖**连接、通道选择、V/I/P 采集**及配套的错误读取。 若后续需要控制负载(CC/CV/CR/CW 设置、输入开关、保护读取、List/动态测试等), 另需扩展命令集,不在本文档范围内。高速暂态波形采集由独立采集卡完成,与本驱动并行运行、互不干扰。