TEP 主端 - Snet Docs

📘 TEP 主端

命名空间: Snet.TEP.master | 类: TepMasterOperate | 基类: DaqAbstract<TepMasterOperate, TepMasterData.Basics> | 接口: IDaq | 包: Snet.TEP

主端是 TEP 的 TCP 服务端(server)。它承载多个 TEP 从端客户端(数据采集设备),并按地址将操作路由到对应客户端。与其余 DAQ 驱动一样,TepMasterOperate 是一个标准 IDaq 驱动——用 OnAsync 打开服务、用 ReadAsync/WriteAsync 读写点位、用 SubscribeAsync 订阅数据变化。从端连接时通过挑战-响应握手认证(UserName/Password 只配置在主端,永不通过线路传输——从端只发送 HMAC-SHA256 证明),随后把采集到的点位值上传给主端;ReadAsync 返回最新缓存值,WriteAsync 向匹配的从端发送写入命令。

Snet.TEP
├── master/
│   ├── TepMasterOperate        // 主端(入口,IDaq)
│   ├── TepMasterData           // Basics 配置
│   └── core/                   // ManageCore、DataCore(客户端会话)
└── slave/                      // TepSlaveOperate(见 [TEP 从端](/tep/tep-slave))
验证

可直接用 Daq 工具 上传 TEP 插件验证主端——无需额外代码。

快速开始

using Snet.TEP.master;
using Snet.Model.data;
using Snet.Model.@enum;
using System.Collections.Concurrent;

// 创建主端——监听 IpAddress:Port,等待从端接入
var master = await TepMasterOperate.InstanceAsync(new TepMasterData.Basics
{
    IpAddress = "127.0.0.1",
    Port = 6688,
    MaxNumber = 1000,
    UserName = "test",     // 从端认证
    Password = "test"
});
await master.OnAsync();

// 批量读取——完整地址 = DevName.DevID.PointName
var address = new Address(new List<AddressDetails>
{
    new("Temperature", "Device1.001.Temperature", DataType.Float),
    new("Pressure", "Device1.001.Pressure", DataType.Float)
});
var result = await master.ReadAsync(address);
if (result.Status)
{
    // ResultData 是以完整地址为键的字典
    var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
    foreach (var kv in data)
        Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}

// 写入——字典键为完整地址
var writeResult = await master.WriteAsync(new ConcurrentDictionary<string, object>
{
    ["Device1.001.Setpoint"] = 100.5f
});

// 订阅(轮询式)——先绑定事件,再订阅
master.OnDataEventAsync += async (sender, e) =>
{
    if (e.Status)
    {
        var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
        foreach (var kv in data)
            Console.WriteLine($"事件数据: {kv.Key} = {kv.Value.ResultValue}");
    }
};
await master.SubscribeAsync(new Address(new AddressDetails("Temperature", "Device1.001.Temperature", DataType.Float)));

await master.OffAsync();
await master.DisposeAsync();

只要有至少一个从端接入并上传过数据,ReadAsync 即可工作。GetStatusAsync 需要至少一个已连接客户端——见 常见问题

安装

dotnet add package Snet.TEP

配置表 —— TepMasterData.Basics

继承 SubscribeData.SCData(轮询订阅参数,见 Snet.Core.subscription)。

参数 类型 默认值 描述
SN string Guid.NewGuid().ToUpperNString() 唯一标识符
IpAddress string? "127.0.0.1" 绑定 IP(正则校验 IPv4)
Port int 6688 绑定端口
MaxNumber int 1000 最大连接数
UserName string? "" 从端身份认证校验的用户名(必须显式配置,否则 OnAsync 失败)
Password string? "" 用于校验从端 HMAC-SHA256 证明的共享密码(必须显式配置,否则 OnAsync 失败)
ChangeOut bool true (继承)订阅:变化时输出
AllOut bool false (继承)当 ChangeOut = true 时:未变化项与变化项一同抛出,保证批次数据完整
HandleInterval int 1000 (继承)轮询间隔——每个订阅周期调用 ReadAsync 的频率(毫秒)
TaskNumber int 5 (继承)订阅任务数量

API 参考

IDaq 生命周期(继承)

方法 描述
Task<OperateResult> OnAsync(CancellationToken token = default) 启动 TCP 服务并开始接收从端连接。每个从端连接时进行身份认证(用户名/密码)与握手
Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default) 取消全局任务循环、停止轮询订阅、关闭 TCP 服务、释放全部客户端会话
Task<OperateResult> GetStatusAsync(CancellationToken token = default) TCP 服务已启动且至少一个客户端已连接时返回 true。有客户端时逐个下发 0x33 状态查询并等待应答(5 秒超时),任一失败则整体失败(无从端时失败:"客户端尚未连接")
Task<OperateResult> GetBaseObjectAsync(CancellationToken token = default) ResultData:底层 TcpServiceOperate
Task<OperateResult> ReadAsync(Address address, CancellationToken token = default) 读取从端上传的最新缓存值:每个地址按 . 拆分为 DevName.DevID.PointName,路由到匹配的客户端会话并从其缓存取值。ResultData:以完整地址为键的 ConcurrentDictionary<string, AddressValue>
Task<OperateResult> WriteAsync(ConcurrentDictionary<string, (object value, EncodingType? encodingType)> values, CancellationToken token = default) (DevName, DevID) 分组,向每个匹配从端发送写入命令(数据写入)并等待其应答(带超时)
Task<OperateResult> SubscribeAsync(Address address, CancellationToken token = default) 基于 SubscribeOperate 的轮询订阅——每 HandleInterval 毫秒调用 ReadAsync 并触发 OnDataEventAsync。首次订阅自动创建订阅组,后续调用向组内追加地址。先绑定事件
Task<OperateResult> UnSubscribeAsync(Address address, CancellationToken token = default) 移除轮询订阅(未订阅时返回失败)

完整的 IDaq 语义(事件系统 OnDataEventAsync / OnInfoEventAsync、虚拟地址等)参见 IDaq 接口

点位地址规则

点位的完整地址为

DevName.DevID.PointName
设备名称.设备ID.点位名称

ReadAsync / WriteAsync. 将地址拆分为设备名称、设备 ID、点位名称,再路由到 DevName/DevID 匹配的客户端会话。因此从端上传的 AddressValue 中的 Key 不能包含 .——请用其他符号代替(如 _-)。

代码示例

通过 IDaq 批量读写

// 混合数据类型、跨多个设备
var mixed = new Address(new List<AddressDetails>
{
    new("Temp1", "Device1.001.Temperature", DataType.Float),
    new("Pres1", "Device1.001.Pressure", DataType.Float),
    new("Temp2", "Device2.001.Temperature", DataType.Float),
    new("Alarm", "Device1.001.Alarm1", DataType.Bool)
});
var read = await master.ReadAsync(mixed);
if (read.Status)
{
    var data = read.GetSource<ConcurrentDictionary<string, AddressValue>>();
    foreach (var kv in data)
        Console.WriteLine($"{kv.Key} = {kv.Value.ResultValue}");
}

// 写入自动按设备分组
var write = await master.WriteAsync(new ConcurrentDictionary<string, object>
{
    ["Device1.001.Setpoint"] = 99.5f,
    ["Device1.001.Speed"] = 42,
    ["Device2.001.Setpoint"] = 30f
});

变化输出订阅

var master = await TepMasterOperate.InstanceAsync(new TepMasterData.Basics
{
    IpAddress = "127.0.0.1",
    Port = 6688,
    HandleInterval = 1000,   // 每秒轮询
    ChangeOut = true         // 只抛变化值
});
await master.OnAsync();

master.OnDataEventAsync += async (sender, e) =>
{
    if (e.Status)
    {
        var data = e.GetSource<ConcurrentDictionary<string, AddressValue>>();
        foreach (var kv in data)
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {kv.Key} = {kv.Value.ResultValue}");
    }
};

await master.SubscribeAsync(new Address(new AddressDetails("Temperature", "Device1.001.Temperature", DataType.Float)));

客户端连接事件

从端连接 / 断开 / 异常通过 OnInfoEventAsync 抛出(在 OnAsync 之前绑定):

master.OnInfoEventAsync += async (sender, e) =>
{
    Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {(e.Status ? "INFO" : "ERROR")}: {e.Message}");
};

常见问题

Q: 服务已启动但 GetStatusAsync 失败? A: GetStatusAsync 要求至少一个已连接客户端——无从端时失败"客户端尚未连接,无法获取客户端状态"。OnAsync 成功仅代表 TCP 服务已开始监听。

Q: ReadAsync 返回"失败"值? A: ReadAsync 读取的是从端上传的缓存值。当没有从端上传过该 DevName.DevID.PointName 时,点位返回失败的 AddressValue——请检查从端是否已连接、认证成功并正常上传。

Q: 为什么点位 Key 不能包含 . A: 主端按 . 拆分完整地址为 DevName.DevID.PointName。点位名称(或从端上传的 Key)中的 . 会破坏拆分。请用 _- 代替。

Q: 从端必须匹配 UserName / Password 吗? A: 是——从端连接时按 basics.UserName / basics.Password 认证。主端会为每次连接发送一次性挑战,从端回复 PasswordProof = {UserName}\n{DevName}\n{DevID}\n{challenge} 的 HMAC-SHA256(大写十六进制),共享密码本身永不传输。TepSlaveOperate C# 库自动完成该过程;原始 SocketTcp 客户端需自行计算证明(见 TEP 从端)。

Q: 订阅是实时的吗? A: 是轮询——SubscribeOperateHandleInterval 毫秒调用 ReadAsync(读从端上传的缓存)。调小间隔更新更快(CPU 占用更高),用 ChangeOut/AllOut 控制抛出哪些值。

Q: 不写代码如何验证主端? A: 使用 Daq 工具——上传 TEP 插件、配置主端,即可从界面读写点位。

相关链接