📡 DaqAbstract 基类
DaqAbstract<O, D> 是所有数据采集协议实现的基类。它继承自 CoreUnify<O, D> 并实现 IDaq。
public abstract class DaqAbstract<O, D> : CoreUnify<O, D>, IDaq
where O : class
where D : class
8个抽象异步方法
每个协议都必须实现这些方法。它们构成了 DAQ 设备的完整生命周期:
1. OnAsync -- 打开连接
public abstract Task<OperateResult> OnAsync(CancellationToken token = default);
建立与设备的物理/逻辑连接。对于 Modbus TCP,此操作打开一个 Socket。对于串行协议,此操作打开 COM 端口。
2. OffAsync -- 关闭连接
public abstract Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default);
关闭连接。hardClose = true 执行强制关闭;false 尝试优雅关闭。
3. ReadAsync -- 读取数据
public abstract Task<OperateResult> ReadAsync(Address address, CancellationToken token = default);
核心数据检索方法。接收一个包含一个或多个 AddressDetails 的 Address 对象。返回带有已解析数据的 OperateResult,数据存放在 ResultData 中。
4. WriteAsync -- 写入数据
// 抽象方法(子类必须实现)
public abstract Task<OperateResult> WriteAsync(
ConcurrentDictionary<string, (object value, EncodingType? encodingType)> values,
CancellationToken token = default);
// 便捷重载 1:自动推导编码(非抽象,委托给抽象方法)
public Task<OperateResult> WriteAsync(
ConcurrentDictionary<string, object> values,
CancellationToken token = default);
// 便捷重载 2:WriteModel 类型转换(非抽象,委托给抽象方法)
public Task<OperateResult> WriteAsync(
ConcurrentDictionary<string, WriteModel> values,
CancellationToken token = default);
将值写入设备地址。抽象方法以 (object value, EncodingType? encodingType) 元组为核心参数形式,子类只实现这一个抽象方法即可。另外两个重载是便捷方法:ConcurrentDictionary<string, object> 版本自动赋予 ASCII 编码后委托给抽象方法;ConcurrentDictionary<string, WriteModel> 版本根据 WriteModel.AddressDataType 逐项类型转换后委托。2026-08 重构起,无效写入值(值为 null、非数字文本、JSON 数组格式错误)在转换阶段即抛出带描述信息的 InvalidCastException,并由该重载的 catch 捕获后以失败 OperateResult 返回({key} 地址数据类型转换异常:{message}),而非在更深的转换链中失败。
5. SubscribeAsync -- 开始订阅
public abstract Task<OperateResult> SubscribeAsync(Address address, CancellationToken token = default);
注册地址以进行连续轮询。已订阅的地址在每个轮询周期触发 OnDataEvent/OnDataEventAsync。
6. UnSubscribeAsync -- 停止订阅
public abstract Task<OperateResult> UnSubscribeAsync(Address address, CancellationToken token = default);
从订阅列表中移除地址。
7. GetStatusAsync -- 设备状态
public abstract Task<OperateResult> GetStatusAsync(CancellationToken token = default);
返回设备的当前连接和运行状态。
8. GetBaseObjectAsync -- 底层对象
public abstract Task<OperateResult> GetBaseObjectAsync(CancellationToken token = default);
返回底层的通信或驱动对象。适用于高级场景。
同步包装方法
上述每个异步方法都有同步便捷包装,内部阻塞等待异步变体完成。仅在非异步上下文中使用(控制台应用、构造函数、同步事件处理器)——异步代码路径请优先使用 Async 版本。
| 方法 | 包装自 |
|---|---|
OperateResult On() |
OnAsync() |
OperateResult Off(bool hardClose = false) |
OffAsync(hardClose) |
OperateResult Read(Address address) |
ReadAsync(address) |
OperateResult Subscribe(Address address) |
SubscribeAsync(address) |
OperateResult UnSubscribe(Address address) |
UnSubscribeAsync(address) |
OperateResult Write(ConcurrentDictionary<string, (object value, EncodingType? encodingType)> values) |
WriteAsync(values) |
OperateResult Write(ConcurrentDictionary<string, object> values) |
WriteAsync(values) |
OperateResult Write(ConcurrentDictionary<string, WriteModel> values) |
WriteAsync(values) |
OperateResult GetStatus() |
GetStatusAsync() |
OperateResult GetBaseObject() |
GetBaseObjectAsync() |
// 同步用法(控制台应用 / 同步上下文)
var op = ModbusOperate.Instance(new ModbusData.Basics { IpAddress = "192.168.1.100", Port = 502, Station = 1, AddressStartWithZero = false });
var status = op.On(); // 阻塞至打开完成
var result = op.Read(new Address(new AddressDetails("Temperature", "1", DataType.Float)));
// ResultData 是按地址名称索引的字典,按名称取值
Console.WriteLine(result.GetSource<ConcurrentDictionary<string, AddressValue>>()["1"].ResultValue);
op.Off(true);
内置地址自动组包(IPacker)
DaqAbstract 直接实现 IPacker 接口,因此每个驱动实例都能直接组包、解包——无需装配处理器(见 IPacker 接口):
| 方法 | 描述 |
|---|---|
Packer / PackerAsync |
智能地址组包——ResultData 为优化后的 Address?(协议族不支持组包时原样透传) |
UnPacker / UnPackerAsync(单值) |
解包单个组包批次 → ConcurrentDictionary<string, AddressValue> |
UnPacker / UnPackerAsync(集合) |
解包一次读取结果的全部批次 → List<ConcurrentDictionary<string, AddressValue>> |
同时暴露两个公共字段,便于直接进行底层访问:
| 字段 | 类型 | 描述 |
|---|---|---|
packerHandler |
PackerHandler |
支撑 Packer 的单例池处理器(PackerHandler.Instance(typeof(O).Name)) |
bytesHandler |
BytesHandler |
支撑 UnPacker 的单例池处理器(BytesHandler.Instance(typeof(O).Name)) |
// 组包 → 读取 → 解包,全部在驱动实例上完成
var packed = await op.PackerAsync(address, "SiemensS7Net_S1200");
if (!packed.Status) return; // 组包失败 / 协议不支持
var packedAddr = packed.GetSource<Address>(); // ResultData = 优化后的 Address
var result = await op.ReadAsync(packedAddr);
if (!result.Status) return; // 读取失败
var data = result.GetSource<ConcurrentDictionary<string, AddressValue>>();
var unpacked = await op.UnPackerAsync(data);
if (!unpacked.Status) return; // 解包失败
var values = unpacked.GetSource<List<ConcurrentDictionary<string, AddressValue>>>();
2个必需属性(来自 CoreUnify)
重写这些属性以便为 GetArgsAsync() 提供元数据:
protected virtual string CN { get; } // 中文显示名称
protected virtual string CD { get; } // 中文描述
6个内置 WebAPI 端点
每个 DaqAbstract 子类通过 HttpServiceOperate 自动暴露 REST API:
| 端点 | 路由 | 方式 | 请求体 |
|---|---|---|---|
| 打开 | /api/on |
POST | 无 |
| 关闭 | /api/off |
POST | 无 |
| 读取 | /api/read |
POST | Address JSON |
| 写入 | /api/write |
POST | ConcurrentDictionary<string, WriteModel> JSON |
| 状态 | /api/getstatus |
GET | 无 |
| 切换语言 | /api/switchlanguage |
POST | 无 |
启用 WebAPI
var wam = new WAModel { Port = 8080 };
await modbus.WAOnAsync(wam); // 在端口 8080 上启动 HTTP 服务器
WebAPI 请求示例
// POST /api/read 的 JSON 请求体:
{
"SN": "8c71f4a7-04eb-4f9c-88d9-849c2f0c3a00",
"AddressArray": [
{
"SN": "TestAddress",
"AddressName": "M100",
"AddressDataType": "Float"
}
],
"CreationTime": "2024-06-05T13:01:24.6245462+08:00"
}
响应:序列化为 JSON 的 OperateResult。
ReadAsync 管道
协议类中 ReadAsync 的实现通常遵循以下流程:
1. 解析 AddressDetails -> 协议特定的地址字节
2. AddressHandler.ExecuteDispose() -- 虚拟地址拦截
3. 构建协议帧(功能码、寄存器、数量)
4. 通过通信层发送帧
5. 接收响应字节
6. 验证响应(CRC、长度、站号)
7. 根据 DataFormat(CDAB/ABCD/等)转换字节
8. 根据 AddressDataType 解析为类型化的 AddressValue
9. 将值填充到 AddressArray -> EventDataResult
10. 触发 OnDataEvent + OnDataEventAsync
11. 返回带有 ResultData 的 OperateResult
AddressHandler.ExecuteDispose() 步骤是在实际 I/O 发生之前拦截虚拟地址的地方。
编码
DaqAbstract 在其静态构造函数中注册 CodePagesEncodingProvider,以支持 GB2312 编码,这是中国制造的 PLC 通常需要的编码。
static DaqAbstract()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
