SSPC-Tester/Driver/IT8702P/IT8702P_Driver_Integration_Guide.md
2026-06-30 12:10:29 +08:00

374 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# IT8702P 电子负载驱动接入文档C# / WPF / .NET 8
> 本文档供 AI 开发助手作为实现依据使用。目标:在 C# + WPF + .NET 8 主项目中,
> 通过 LAN Socket 接入 ITECH IT8702P 电子负载,采集**电压 / 电流 / 功率**三项数据,
> 既能实时绑定到界面显示,也能由后台连续记录。
>
> **AI 开发约束**:严格遵循本文协议与命令格式。不要臆造未在本文出现的 SCPI 命令;
> 命令一律以 `\n` 结尾;查询命令必须读完整行响应后再发下一条;设置类命令前必须已发送 `SYST:REM`。
---
## 1. 通讯概览
| 项目 | 值 |
|------|-----|
| 物理接口 | LAN以太网 |
| 传输协议 | 标准 TCPraw socket / SCPI-RAW与普通 socket 编程完全一致 |
| 默认 IP | `192.168.200.100`(以设备实际配置为准,需可在设置中修改) |
| 端口 | `30000` |
| 命令语言 | SCPIASCII 文本,大小写不敏感) |
| 命令结束符 | `\n`LF0x0A |
| 响应结束符 | `\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` 取 18。默认 `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)` 解析。
> **`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
```
错误处理约定:
- 每条查询发送后若在超时内(建议 13 s未读到完整行 → 视为通讯异常,触发重连流程。
- 周期性(或异常后)发送 `SYST:ERR?` 检查设备侧错误队列,非 `0,No Error` 则记录日志。
- 读响应以 `\n` 为行边界TCP 可能分包,需循环 `Read` 直到读到 `\n`
---
## 4. C# 实现指引(.NET 8
### 4.1 分层结构建议
```
Driver 层 It8702pClient —— 纯通讯:连接、收发、按行读取、命令封装
采集层 AcquisitionService —— 后台采集循环Task + PeriodicTimer产出读数
分发层 读数 → ① UI 绑定INotifyPropertyChangedDispatcher 回主线程)
→ ② 记录器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 数据模型
```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<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 采集服务参考实现
```csharp
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 绑定要点
```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<T>(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` | 操作通道,可切换 18 |
| `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/动态测试等),
另需扩展命令集,不在本文档范围内。高速暂态波形采集由独立采集卡完成,与本驱动并行运行、互不干扰。