📘 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: 是轮询——SubscribeOperate 每 HandleInterval 毫秒调用 ReadAsync(读从端上传的缓存)。调小间隔更新更快(CPU 占用更高),用 ChangeOut/AllOut 控制抛出哪些值。
Q: 不写代码如何验证主端? A: 使用 Daq 工具——上传 TEP 插件、配置主端,即可从界面读写点位。
相关链接
- TEP 协议概览 — 架构与两种对接方式
- TEP 从端 — 数据采集端(C# 动态库 + 原始 SocketTcp 协议)
- IDaq 接口 · DaqAbstract 基类
- 地址模型
