Chapter 1 起步
你是否纠结过:如何避免为各种常见的工业硬件编写“重复”的代码?
StdUnit.Tags 是我编写的一套面向工业硬件交互场景的极简类库。无论底层通信是OpcUa、S7、还是ModbusTcp,或者是串口、甚至可能是MQTT,StdUnit.Tags 都能让你以统一的方式,编写横跨各种工业硬件的交互代码。它有如下几个设计理念:
- 模块化+可扩展: 各模块之间互相独立,没有耦合。以
.AddS7Support()的方式添加S7通信支持,以.AddComScannerSupport()方式添加串口扫码枪通信支持……没发现你想要的硬件支持?你可以自己轻易扩展。 - 允许组合读写:可以用整体读写的方式来减少IO次数。
- 刚好够用:只提供刚好够用的功能,简单易学,符合直觉,没有魔法。
这个类库是前些年我过年放假在家时编写的,主要目的是为了降低门槛(足够简单)、提高灵活性。起初,我只想做一个通信类库以自用,所以当时冠名
Itminus.Tags。后来又逐步引入了业务逻辑插件化、串行轮询扫描等等一些值得复用的手段,所以自0.15.1之后,原来的Itminus.Tags改名成StdUnit.Tags.*,寓意这是一个可重复使用的标准单元。StdUnit.Tags自0.16.0开始在测试源上发布,自1.0.0起发布到 nuget.org;老的Itminus.Tags系列依将永久保持可用状态,但不再更新。
在着手编写这个项目之前,我倾向于使用强类型的方式,在类型实例和一段连续的字节数组之间做递归式地自动映射,这种做法运行效率非常高,而且还有超级友好的类型提示,我曾偏爱这种代码风格接近十年。但不可否认,这也带来了两个坏处:
一是要求编程人员熟知内存布局,比如要求大家知道
int在C#中会占据4个字节,而在西门子PLC中占据2个字节;再比如,编程者需要知道西门子PLC的字符串会在头部附加两个字节,用来存储总字节数和有效字节数。这一点无疑拔高了编程门槛。二是点表和程序代码一一对应的,这意味着动态修改比较麻烦。熟悉我的朋友都知道,我一贯强烈反对把修改代码的工作延迟到现场,但是我直到最近才认可这和提供灵活性不完全冲突。
快速上手
我们提供两种方式让你快速跑起一个测点项目。
使用模板创建新项目
对于全新项目,我们推荐你使用模板StdUnit.Tags.Templates:
dotnet new install StdUnit.Tags.Templates
创建Web项目:
dotnet new tags.web
创建WPF项目:
dotnet new tags.wpf
你也可以创建 target 到
net472的 WPF 项目:dotnet new tags.wpf-netfx不过,在
netfx下,AspNetCore相关功能(含Blazor)不可用,插件系统需要降级使用(不支持卸载、热更等)。
创建Lib项目:
dotnet new tags.lib
手动创建新项目/添加到已有项目
对于想一步一步手动创建的用户,也可以手动在你的项目中添加依赖、注册相关服务和硬件支持。
依赖包
首先,我们需要添加基础包依赖:
dotnet add package StdUnit.Tags
然后,根据项目所需要通信的底层硬件,选择一个或者多个具体的实现包。目前内置的硬件支持包括:
| 驱动 | 包名 | 备注 |
|---|---|---|
| 简单文件 | StdUnit.Tags.SimpleFiles | 支持 |
| 西门子S7 | StdUnit.Tags.S7 | 支持 |
| OpcUA | StdUnit.Tags.OpcUa | 支持 |
| 华杰智控IO盒子 | StdUnit.Tags.Hjzk | 支持 |
| ModbusTcp | StdUnit.Tags.ModbusTcp | 实验 |
| 串口 | StdUnit.Tags.ComScanner | 实验 |
| 卓兰IO盒子 | StdUnit.Tags.ZLan | 实验 |
StdUnit.Tags支持的协议和硬件会持续增加。如果暂时没有你想要的,可以自由扩展实现你自己的驱动。
然后向注册相关服务和硬件支持:
services.AddTagsProjectServices(builder =>
{
builder.AddS7Support();
// 增加其它更多的硬件支持,比如:
// builder.AddComScannerSupport();
// ...
});
如果你喜欢响应式,可以添加下面两个扩展包(之一):
StdUnit.Tags.RxExtensions
StdUnit.Tags.R3Extensions
如果你需要接入AI,可以引入StdUnit.Tags.McpServer这个扩展包,把你的硬件暴露给Agent。
项目描述
通常,我们会在本地目录下创建一份测点项目描述文件(index.xml),内容类似于:
<?xml version="1.0" encoding="utf-8"?>
<Project>
<!-- 通道,可以配置多个-->
<Channel name="S7-1" driver="S7" >
<IpAddr>172.16.10.20</IpAddr>
<Rack>0</Rack>
<Slot>1</Slot>
</Channel>
<!-- 测点配置,可以配置多个 -->
<TagGrp name="IoBox" isEntry="true" isEnabled="true" channel="S7-1" scanInterval="0">
<TagGrp name="通用状态">
<TagCbnt name="PLC" address="DB200.100">
<Tag name="心跳请求" address="$$100.0" type="BIT" access="RO"></Tag>
</TagCbnt>
<TagCbnt name="MST" address="DB201.100" access="R1W">
<Tag name="心跳响应" address="$$100.0" type="BIT"></Tag>
</TagCbnt>
</TagGrp>
</TagGrp>
<!-- 逻辑配置,可以配置0~N个 -->
<!--<Logicet>Samples.Plugin1.dll</Logicet>-->
</Project>
备注:除了使用约定的
index.xml,我们也可以用程序的方式自己构建节点,这样项目工厂就不会去搜寻index.xml文件了。
备注:XML没有智能提示和自动校验,让你没有安全感?可以考虑引入我编写的XSD,参见配置和构建
业务逻辑
让我们创建一个心跳逻辑来处理心跳随动信号:
public class HeartBeatLogicet : LogicetBase
{
private readonly ITag heartReq;
private readonly ITag heartAck;
private readonly ILogger<HeartBeatLogicet> _logger;
public HeartBeatLogicet(ITagsProject project, ILogger<HeartBeatLogicet> logger)
: base(project)
{
var grp = this.Tags.SelectGrp("IoBox/通用状态");
heartReq = grp.SelectTag("PLC/心跳请求");
heartAck = grp.SelectTag("MST/心跳响应");
this._logger = logger;
}
public override int Order => 2;
public override bool MatchEntry(ITagGrp entry) => entry.TagName() == "IoBox";
public override Task ProcessAsync(ITagGrp entry, ITagChannel? thisChannel)
{
heartAck.Value = heartReq.Value;
return Task.CompletedTask;
}
}
项目运行
上面的index.xml仅仅只是一份测点项目静态描述。为了将该项目跑起来,最简单的思路是利用ITagsProjectCtrl接口提供的.StartPollAsync()方法:
using var scope = this._ssf.CreateScope();
var sp = scope.ServiceProvider;
// 项目启停控制器
var ctrl = sp.GetRequiredService<ITagsProjectCtrl>();
var dir ="D:/manufacture/pl01/"; // 提供项目运行目录,其中有通信点表和可能用到的插件
XElement? root = null; // 空表示使用默认的`index.xml`来配置项目
// 启动测点项目串行轮询,此方法通常会长时间运行:
// 即使发生异常,也不会主动结束,而是会自动重试;
// 除非在启动阶段发生异常或者测点项目被停止,否则不会主动退出。
await ctrl.StartPollAsync(dir, root, hook: async(proj, sp, ct) =>{
// 添加业务逻辑
proj.Logicets.Add(new HeartBeatLogicet(
proj,
loggerFactory.CreateLogger<HeartBeatLogicet>()
));
// ... 可选:但推荐注册 proj.RunnerStarted 事件处理
// ... 可选,但推荐注册 proj.RunnerCrashed 事件处理
});
项目停止
如果想在某个时刻停止项目,调用.StopAsync()方法即可:
await ctrl.StopAsync();
项目停止后,与项目相关资源会被自动释放。如果需要重新启动项目,则需要重新调用.StartAsync(dir, root, hook)方法。
备注
如果我的代码能帮到你,我们希望你也能在力所能及的情况下,回馈开源。程序员的生涯也许是非常短暂的,但是一旦你的作品开始发出微弱的光芒,就能轻松照亮无数的后来者以快步前行。
如果需要和我探讨编程技术,可以通过(aXRtaW51c0Bob3RtYWlsLmNvbQ==)联系我。或许在我有空的时候,大家可以畅聊人生。
由于个人精力有限,我只能优先实现我自己需要的特性。对于一些尚未支持的通信协议,我们鼓励你自己动手写实现。如果你自己不想花太多精力,也可以付费购买其他开发者的时间。