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

17 KiB
Raw Permalink Blame History

UDP5080-100 驱动接口开发指导文档

本文档用于指导 AI 在主项目中接入 UNI-T UDP5080-100 可编程直流电源。主项目技术栈为 C# + WPF + .NET 8。将本文档与 UNI-T SDK 目录一起拷贝到主项目后AI 应优先按本文档实现驱动层,不要直接照搬 SDK 示例里的旧机型命令。

1. 接入结论

UDP5080-100 当前推荐接入方式是 SCPI over TCP Socket

  • 默认设备 IP192.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.dllucics.dll 和 C# 示例主要面向 UCI 协议设备、USB/libusb 设备、示波器、旧款电源等,不应作为 UDP5080-100 的首选依赖。

2. 现场网络准备

设备通过网线连接电脑。电脑有线网卡需设置为与设备同网段,例如:

  • 电脑 IP192.168.6.10
  • 子网掩码:255.255.255.0
  • 网关:192.168.1.1

接入前先确认:

ping 192.168.6.5

MAC 地址 00-80-E1-5C-CB-CD 仅用于核对设备身份,不作为驱动连接参数。

3. 推荐项目结构

在主项目中增加独立驱动目录,避免把 UI 逻辑和设备通信混在一起:

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. 驱动接口设计

建议接口如下:

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);
}

数据模型:

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 保护 WriteAsyncQueryAsync
  • 网络异常、超时、解析失败统一包装成 Udp5080Exception
  • 断线后 IsConnected 置为 false,上层决定是否重连。
  • UI 线程不得直接执行网络 I/O。

最小实现示例:

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();
    }
}

异常类型:

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/设备电源。

采样伪代码:

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 调试和自动化测试:

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.dllC# UCI 封装。
  • lib/C/Unicode/uci.dlllib/C/x64/Unicode/uci.dllUCI 原生 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# 示例中的典型调用:

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 或项目已有配置系统:

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