506 lines
17 KiB
Markdown
506 lines
17 KiB
Markdown
# 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 日志和用户提示使用。
|