14 KiB
14 KiB
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? |
查询 | 读取错误队列下一条,格式 <NR1>,<SRD>,如 0,"No Error"。无错时返回 0,No Error |
*CLS |
设置 | 清除状态/错误队列 |
2.2 通道选择(多模块寻址)
| 命令 | 类型 | 说明 |
|---|---|---|
CHAN <n> |
设置 | 选择当前操作通道,n 取 1–8。默认 CHAN 1。后续命令作用于该通道 |
CHAN? |
查询 | 返回当前选中通道号 <NR1> |
CHAN:ID? |
查询 | 返回当前通道模块的型号/序列号/固件,逗号分隔 |
单通道使用时,连接后发一次
CHAN 1即可;切换通道时重发CHAN <n>。
2.3 测量(核心:电压 / 电流 / 功率)
| 命令 | 类型 | 返回 | 说明 |
|---|---|---|---|
MEAS:VOLT? |
查询 | <NR3> |
触发一次新测量并返回电压(V)。较慢 |
MEAS:CURR? |
查询 | <NR3> |
触发一次新测量并返回电流(A) |
MEAS:POW? |
查询 | <NR3> |
触发一次新测量并返回功率(W) |
FETC:VOLT? |
查询 | <NR3> |
取最近一次采样的电压,不重新测量。较快 |
FETC:CURR? |
查询 | <NR3> |
取最近采样电流 |
FETC:POW? |
查询 | <NR3> |
取最近采样功率 |
返回值为 ASCII 科学计数字符串,如 2.971700E+00,用 double.Parse(s, CultureInfo.InvariantCulture) 解析。
MEASvsFETC:MEAS每次强制重新测量,数值最新但慢;FETC取设备已有采样,快但可能略滞后。连续轮询场景推荐FETC以提升刷新率。
2.4 合并查询(一次往返取三个值,强烈推荐)
SCPI 允许用分号在一条消息里串多个查询,一次往返拿回多值,往返次数从 3 次降到 1 次:
MEAS:VOLT?;:MEAS:CURR?;:MEAS:POW?
- 第二条及之后每条查询前加
:,表示回到命令根路径(否则会拼接出错误路径)。 - 响应为分号分隔的一行,如
2.971700E+00;1.040000E+00;3.090000E+00,按;拆分解析。 FETC同理可合并:FETC:VOLT?;:FETC:CURR?;:FETC:POW?
3. 标准交互流程
[连接]
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<Reading> 写入文件/数据库,独立消费者)
ViewModel 层 绑定 Voltage / Current / Power 与 IsConnected 等状态
设计要点(对应"实时绑定 + 后台记录两者都要"):
- 采集只跑一遍,结果同时分发给 UI 和记录器,避免对设备发起两套独立轮询(同步协议下并发查询会互相打断)。
- UI 更新必须切回 UI 线程:
Application.Current.Dispatcher.Invoke(...)或在采集 Task 中用IProgress<Reading>。 - 后台记录用
System.Threading.Channels.Channel<Reading>:采集线程只管入队(非阻塞),单独的写盘消费者出队,互不阻塞,避免磁盘 IO 拖慢采集节奏。 - 所有 socket 收发集中在 Driver 层,全程串行(同一时刻只有一条命令在途)。如有多处调用,用
SemaphoreSlim(1,1)串行化,杜绝Query Interrupted。
4.2 数据模型
public readonly record struct Reading(
DateTime TimestampUtc,
int Channel,
double Voltage, // V
double Current, // A
double Power); // W
4.3 Driver 层参考实现
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<string> 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<string> 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<string> 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 采集服务参考实现
using System.Threading.Channels;
public sealed class AcquisitionService : IAsyncDisposable
{
private readonly It8702pClient _client;
private readonly Channel<Reading> _record = Channel.CreateUnbounded<Reading>();
private CancellationTokenSource? _cts;
private Task? _loop;
public AcquisitionService(It8702pClient client) => _client = client;
/// 供后台记录器消费的读数流。
public ChannelReader<Reading> Records => _record.Reader;
/// UI 通过此回调实时获取读数(在 ViewModel 中切回 UI 线程)。
public event Action<Reading>? 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 绑定要点
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<T>(ref T field, T value, [System.Runtime.CompilerServices.CallerMemberName] string? name = null)
{ field = value; PropertyChanged?.Invoke(this, new(name)); }
}
后台记录消费者(独立运行,写文件/数据库):
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/动态测试等), 另需扩展命令集,不在本文档范围内。高速暂态波形采集由独立采集卡完成,与本驱动并行运行、互不干扰。