SSPC-Tester/Driver/UDP5080-100/驱动接口开发指导文档.md

506 lines
17 KiB
Markdown
Raw Normal View History

# UDP5080-100 驱动接口开发指导文档
本文档用于指导 AI 在主项目中接入 UNI-T UDP5080-100 可编程直流电源。主项目技术栈为 C# + WPF + .NET 8。将本文档与 `UNI-T SDK` 目录一起拷贝到主项目后AI 应优先按本文档实现驱动层,不要直接照搬 SDK 示例里的旧机型命令。
## 1. 接入结论
UDP5080-100 当前推荐接入方式是 `SCPI over TCP Socket`
- 默认设备 IP`192.168.6.5`
- 默认端口:`5025`
- 默认终止符:`\n`
- 默认连接超时:`2s`
- 默认采样周期:`0.5s`
- VISA 资源参考:`TCPIP0::192.168.6.5::INSTR`
- Socket 资源参考:`TCPIP0::192.168.6.5::5025::SOCKET`
推荐主项目直接用 .NET 的 `TcpClient` / `NetworkStream` 实现 TCP 5025 通信。`UNI-T SDK` 中的 `uci.dll`、`ucics.dll` 和 C# 示例主要面向 UCI 协议设备、USB/libusb 设备、示波器、旧款电源等,不应作为 UDP5080-100 的首选依赖。
## 2. 现场网络准备
设备通过网线连接电脑。电脑有线网卡需设置为与设备同网段,例如:
- 电脑 IP`192.168.6.10`
- 子网掩码:`255.255.255.0`
- 网关:`192.168.1.1`
接入前先确认:
```powershell
ping 192.168.6.5
```
MAC 地址 `00-80-E1-5C-CB-CD` 仅用于核对设备身份,不作为驱动连接参数。
## 3. 推荐项目结构
在主项目中增加独立驱动目录,避免把 UI 逻辑和设备通信混在一起:
```text
MainProject/
Drivers/
UniT/
Udp5080/
IUdp5080PowerSupply.cs
Udp5080Options.cs
Udp5080Reading.cs
Udp5080Exception.cs
Udp5080TcpClient.cs
Udp5080Simulator.cs
UNI-T SDK/
```
WPF ViewModel 只依赖 `IUdp5080PowerSupply`,不要直接依赖 `TcpClient`、SDK DLL 或 SCPI 字符串。
## 4. 驱动接口设计
建议接口如下:
```csharp
public interface IUdp5080PowerSupply : IAsyncDisposable
{
bool IsConnected { get; }
Task ConnectAsync(CancellationToken cancellationToken = default);
Task DisconnectAsync(CancellationToken cancellationToken = default);
Task<string> QueryIdAsync(CancellationToken cancellationToken = default);
Task<double> ReadVoltageAsync(CancellationToken cancellationToken = default);
Task<double> ReadCurrentAsync(CancellationToken cancellationToken = default);
Task<Udp5080Reading> ReadOutputAsync(CancellationToken cancellationToken = default);
Task WriteAsync(string command, CancellationToken cancellationToken = default);
Task<string> QueryAsync(string command, CancellationToken cancellationToken = default);
}
```
数据模型:
```csharp
public sealed record Udp5080Options(
string Host = "192.168.6.5",
int Port = 5025,
TimeSpan Timeout = default,
string Terminator = "\n",
string VoltageCommand = "MEASure:VOLTage?",
string CurrentCommand = "MEASure:CURRent?");
public sealed record Udp5080Reading(
DateTimeOffset Timestamp,
double Voltage,
double Current)
{
public double Power => Voltage * Current;
}
```
注意:`TimeSpan Timeout = default` 在构造后应转换为 `TimeSpan.FromSeconds(2)`
## 5. SCPI 命令约定
已验证/调试工具默认使用的基础命令:
| 功能 | 命令 |
| --- | --- |
| 设备标识 | `*IDN?` |
| 读取输出电压 | `MEASure:VOLTage?` |
| 读取输出电流 | `MEASure:CURRent?` |
设备响应中可能包含单位或前缀,解析时应提取第一个浮点数,支持科学计数法。例如:
- `48.125`
- `+1.25E+01 V`
- `VOLT: 48.000 V`
不要把 SDK 文档或示例中的 `:SOURce1:VOLTage?` 直接当作实时测量命令。该类命令可能查询的是设定值,不一定是 UDP5080-100 的实时输出值。
## 6. TCP 实现要点
实现 `Udp5080TcpClient` 时遵守以下规则:
- 每条命令发送 ASCII 文本,并追加 `\n`
- `QueryAsync` 发送命令后读取到换行符为止。
- 单次响应设置上限,例如 `1 MB`,避免异常设备导致内存无限增长。
- 同一个设备连接必须串行化读写,使用 `SemaphoreSlim` 保护 `WriteAsync``QueryAsync`
- 网络异常、超时、解析失败统一包装成 `Udp5080Exception`
- 断线后 `IsConnected` 置为 `false`,上层决定是否重连。
- UI 线程不得直接执行网络 I/O。
最小实现示例:
```csharp
using System.Globalization;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
public sealed class Udp5080TcpClient : IUdp5080PowerSupply
{
private readonly Udp5080Options _options;
private readonly SemaphoreSlim _gate = new(1, 1);
private TcpClient? _client;
private NetworkStream? _stream;
public bool IsConnected => _client?.Connected == true && _stream is not null;
public Udp5080TcpClient(Udp5080Options options)
{
var timeout = options.Timeout == default ? TimeSpan.FromSeconds(2) : options.Timeout;
_options = options with { Timeout = timeout };
}
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
await DisconnectAsync(cancellationToken);
var client = new TcpClient { NoDelay = true };
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_options.Timeout);
await client.ConnectAsync(_options.Host, _options.Port, cts.Token);
_client = client;
_stream = client.GetStream();
}
public Task DisconnectAsync(CancellationToken cancellationToken = default)
{
_stream?.Dispose();
_client?.Dispose();
_stream = null;
_client = null;
return Task.CompletedTask;
}
public Task<string> QueryIdAsync(CancellationToken cancellationToken = default)
=> QueryAsync("*IDN?", cancellationToken);
public async Task<double> ReadVoltageAsync(CancellationToken cancellationToken = default)
=> ParseNumber(await QueryAsync(_options.VoltageCommand, cancellationToken));
public async Task<double> ReadCurrentAsync(CancellationToken cancellationToken = default)
=> ParseNumber(await QueryAsync(_options.CurrentCommand, cancellationToken));
public async Task<Udp5080Reading> ReadOutputAsync(CancellationToken cancellationToken = default)
{
var voltage = await ReadVoltageAsync(cancellationToken);
var current = await ReadCurrentAsync(cancellationToken);
return new Udp5080Reading(DateTimeOffset.Now, voltage, current);
}
public async Task WriteAsync(string command, CancellationToken cancellationToken = default)
{
await _gate.WaitAsync(cancellationToken);
try
{
var stream = RequireStream();
var payload = Encoding.ASCII.GetBytes(command.TrimEnd('\r', '\n') + _options.Terminator);
await stream.WriteAsync(payload, cancellationToken);
await stream.FlushAsync(cancellationToken);
}
catch (Exception ex) when (ex is IOException or SocketException or OperationCanceledException)
{
throw new Udp5080Exception($"发送命令失败:{command}", ex);
}
finally
{
_gate.Release();
}
}
public async Task<string> QueryAsync(string command, CancellationToken cancellationToken = default)
{
await _gate.WaitAsync(cancellationToken);
try
{
var stream = RequireStream();
var payload = Encoding.ASCII.GetBytes(command.TrimEnd('\r', '\n') + _options.Terminator);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_options.Timeout);
await stream.WriteAsync(payload, cts.Token);
await stream.FlushAsync(cts.Token);
var buffer = new byte[4096];
using var memory = new MemoryStream();
while (memory.Length < 1024 * 1024)
{
var read = await stream.ReadAsync(buffer, cts.Token);
if (read <= 0)
throw new Udp5080Exception("设备已关闭连接。");
memory.Write(buffer, 0, read);
if (Array.IndexOf(buffer, (byte)'\n', 0, read) >= 0)
break;
}
return Encoding.ASCII.GetString(memory.ToArray()).Trim();
}
catch (Udp5080Exception)
{
throw;
}
catch (Exception ex) when (ex is IOException or SocketException or OperationCanceledException)
{
throw new Udp5080Exception($"查询命令失败:{command}", ex);
}
finally
{
_gate.Release();
}
}
private NetworkStream RequireStream()
{
if (_stream is null)
throw new Udp5080Exception("设备尚未连接。");
return _stream;
}
private static double ParseNumber(string response)
{
var match = Regex.Match(
response.Trim(),
@"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?");
if (!match.Success)
throw new Udp5080Exception($"响应中没有数值:{response}");
var value = double.Parse(match.Value, CultureInfo.InvariantCulture);
if (!double.IsFinite(value))
throw new Udp5080Exception($"响应数值无效:{response}");
return value;
}
public async ValueTask DisposeAsync()
{
await DisconnectAsync();
_gate.Dispose();
}
}
```
异常类型:
```csharp
public sealed class Udp5080Exception : Exception
{
public Udp5080Exception(string message) : base(message) { }
public Udp5080Exception(string message, Exception innerException) : base(message, innerException) { }
}
```
## 7. WPF 集成要求
ViewModel 中建议使用后台轮询任务:
- `ConnectCommand`:调用 `ConnectAsync`,成功后调用 `QueryIdAsync` 显示设备标识。
- `StartSamplingCommand`:启动后台采样循环。
- `StopSamplingCommand`:取消采样 `CancellationTokenSource`
- `DisconnectCommand`:先停止采样,再断开设备。
- 采样循环中调用 `ReadOutputAsync`,将结果通过 UI Dispatcher 或 MVVM 框架的主线程调度更新到绑定属性。
- 连续通信失败 3 次后自动停止采样,并提示用户检查网线/IP/设备电源。
采样伪代码:
```csharp
private async Task SamplingLoopAsync(CancellationToken token)
{
var failures = 0;
while (!token.IsCancellationRequested)
{
try
{
var reading = await _powerSupply.ReadOutputAsync(token);
failures = 0;
await _dispatcher.InvokeAsync(() =>
{
Voltage = reading.Voltage;
Current = reading.Current;
Power = reading.Power;
Samples.Add(reading);
});
}
catch (Exception ex) when (ex is Udp5080Exception or IOException or SocketException)
{
failures++;
LogCommunicationError(ex);
if (failures >= 3)
{
await StopSamplingAsync();
break;
}
}
await Task.Delay(TimeSpan.FromMilliseconds(500), token);
}
}
```
## 8. 模拟驱动
主项目应保留模拟驱动便于无实机开发、UI 调试和自动化测试:
```csharp
public sealed class Udp5080Simulator : IUdp5080PowerSupply
{
private readonly DateTimeOffset _started = DateTimeOffset.Now;
public bool IsConnected { get; private set; }
public Task ConnectAsync(CancellationToken cancellationToken = default)
{
IsConnected = true;
return Task.CompletedTask;
}
public Task DisconnectAsync(CancellationToken cancellationToken = default)
{
IsConnected = false;
return Task.CompletedTask;
}
public Task<string> QueryIdAsync(CancellationToken cancellationToken = default)
=> Task.FromResult("UNI-T,UDP5080-100,SIM0001,1.0");
public Task<double> ReadVoltageAsync(CancellationToken cancellationToken = default)
{
var seconds = (DateTimeOffset.Now - _started).TotalSeconds;
return Task.FromResult(48.0 + 1.5 * Math.Sin(seconds / 4));
}
public Task<double> ReadCurrentAsync(CancellationToken cancellationToken = default)
{
var seconds = (DateTimeOffset.Now - _started).TotalSeconds;
return Task.FromResult(8.0 + 0.8 * Math.Sin(seconds / 2.7));
}
public async Task<Udp5080Reading> ReadOutputAsync(CancellationToken cancellationToken = default)
{
var voltage = await ReadVoltageAsync(cancellationToken);
var current = await ReadCurrentAsync(cancellationToken);
return new Udp5080Reading(DateTimeOffset.Now, voltage, current);
}
public Task WriteAsync(string command, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public async Task<string> QueryAsync(string command, CancellationToken cancellationToken = default)
{
var upper = command.ToUpperInvariant();
if (upper.Contains("*IDN?"))
return await QueryIdAsync(cancellationToken);
if (upper.Contains("VOLT"))
return (await ReadVoltageAsync(cancellationToken)).ToString("F5", CultureInfo.InvariantCulture);
if (upper.Contains("CURR"))
return (await ReadCurrentAsync(cancellationToken)).ToString("F5", CultureInfo.InvariantCulture);
return command.TrimEnd().EndsWith("?") ? "0" : string.Empty;
}
public ValueTask DisposeAsync()
{
IsConnected = false;
return ValueTask.CompletedTask;
}
}
```
## 9. UNI-T SDK 使用边界
`UNI-T SDK` 目录内容:
- `doc/`各机型编程手册、UCI API 帮助文档。
- `driver/`USB 转串口、libusb 等驱动安装程序。
- `include/`C/C++ 头文件。
- `lib/C#/ucics.dll`C# UCI 封装。
- `lib/C/Unicode/uci.dll`、`lib/C/x64/Unicode/uci.dll`UCI 原生 DLL。
- `examples/`VC、VB、C#、CVI、LabView 示例。
只有在确认 UDP5080-100 现场固件要求 UCI 或 VISA而 TCP 5025 不可用时,才考虑 SDK 路线:
1. C# 项目引用 `UNI-T SDK/lib/C#/ucics.dll`
2. 将匹配位数和编码的 `uci.dll` 复制到程序输出目录。
3. .NET 8 WPF 主程序建议强制 `x64`,避免 AnyCPU 与原生 DLL 位数不一致。
4. C# 示例入口是 `UNI-T SDK/examples/UCI/C#/SimpleIO/Program.cs`
5. UCI 接口返回值 `< 0` 表示错误,通过 `Session.GetLastError()``UCIInterop.GetLastError()` 获取错误描述。
6. 如果 `Open` 返回 `-1040` 一类连接字符串格式错误,常见原因是 C# 工程与 `uci.dll` 字符编码版本不一致C# 通常使用 Unicode 版本。
UCI C# 示例中的典型调用:
```csharp
using uci;
var session = new Session(true);
UCIInterop.SetAttribute(UCIInterop.InvalidSession, "lang:zh-Hans;", null, 0);
var nodes = new uci.Node[10];
var count = UCIInterop.QueryNodesX("LAN:5000,4162,8000,18191;", nodes, nodes.Length, 2000);
if (count > 0)
{
var result = session.Open(nodes[0].UCIAddr);
if (Session.Error(result))
throw new Exception(session.GetLastError());
}
```
再次强调SDK/UCI 是备选路径,不是本项目 UDP5080-100 TCP 接入的默认路径。
## 10. 配置建议
将设备连接参数放入主项目配置,例如 `appsettings.json` 或项目已有配置系统:
```json
{
"Udp5080": {
"Mode": "Tcp",
"Host": "192.168.6.5",
"Port": 5025,
"TimeoutSeconds": 2,
"SampleIntervalMs": 500,
"VoltageCommand": "MEASure:VOLTage?",
"CurrentCommand": "MEASure:CURRent?"
}
}
```
UI 应允许现场修改 IP、端口、超时、采样周期、测量命令并持久化保存。
## 11. 测试与验收清单
无实机测试:
- 使用 `Udp5080Simulator` 验证连接、采样、曲线、日志、停止采样。
- 单元测试 `ParseNumber`,覆盖普通小数、科学计数法、带单位文本、无效响应。
- 验证连续 3 次异常后采样停止。
- 验证断开连接时后台任务能被取消,不阻塞 WPF 关闭。
实机测试:
- 电脑网卡与设备处于 `192.168.6.x` 同网段。
- `ping 192.168.6.5` 成功。
- `ConnectAsync` 成功,`*IDN?` 有响应。
- `MEASure:VOLTage?` 返回合理电压。
- `MEASure:CURRent?` 返回合理电流。
- 采样 5 分钟无 UI 卡顿、无内存持续增长。
- 拔网线后能提示通信失败,且不会卡死 UI。
- 恢复网线后可手动重新连接。
## 12. AI 开发注意事项
后续 AI 在主项目中开发时必须遵守:
- 不要在 WPF code-behind 中直接写 socket 通信。
- 不要在 UI 线程中阻塞等待 `.Result``.Wait()`
- 不要把 SDK 示例里的示波器命令、UTP3000C 命令直接套到 UDP5080-100。
- 不要默认安装 NI-VISA、libusb 或 USB 转串口驱动TCP 5025 不需要这些驱动。
- 不要把 `UNI-T SDK` 下的 exe、示例 bin/obj 全部复制到输出目录,只复制实际依赖项。
- 驱动层要可替换,至少提供 TCP 实现和 Simulator 实现。
- 所有设备异常都应转成清晰的中文错误信息,供 UI 日志和用户提示使用。