# 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 QueryIdAsync(CancellationToken cancellationToken = default); Task ReadVoltageAsync(CancellationToken cancellationToken = default); Task ReadCurrentAsync(CancellationToken cancellationToken = default); Task ReadOutputAsync(CancellationToken cancellationToken = default); Task WriteAsync(string command, CancellationToken cancellationToken = default); Task 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 QueryIdAsync(CancellationToken cancellationToken = default) => QueryAsync("*IDN?", cancellationToken); public async Task ReadVoltageAsync(CancellationToken cancellationToken = default) => ParseNumber(await QueryAsync(_options.VoltageCommand, cancellationToken)); public async Task ReadCurrentAsync(CancellationToken cancellationToken = default) => ParseNumber(await QueryAsync(_options.CurrentCommand, cancellationToken)); public async Task 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 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 QueryIdAsync(CancellationToken cancellationToken = default) => Task.FromResult("UNI-T,UDP5080-100,SIM0001,1.0"); public Task ReadVoltageAsync(CancellationToken cancellationToken = default) { var seconds = (DateTimeOffset.Now - _started).TotalSeconds; return Task.FromResult(48.0 + 1.5 * Math.Sin(seconds / 4)); } public Task 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 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 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 日志和用户提示使用。