SSPC-Tester/Driver/UDP5080-100/驱动接口开发指导文档.md
2026-06-30 12:10:29 +08:00

506 lines
17 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.

# 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 日志和用户提示使用。