Chapter 2 基本概念
Itminus.Tags用测点项目(ITagsProject)来组织各个模块:
\( \boxed{\text{测点项目}} = \boxed{\text{通道}} + \boxed{\text{测点}} + \boxed{\text{逻辑}} \)
通道(ITagChannel)
顾名思义,它规定了和底层硬件通信的信息“通道”:
public interface ITagChannel : IDisposable
{
/// <summary>
/// 通道名称
/// </summary>
string ChannelName { get; }
/// <summary>
/// 驱动
/// </summary>
string Driver { get; }
/// <summary>
/// 连接
/// </summary>
/// <returns></returns>
Task EnsureConnectedAsync(bool force, CancellationToken ct);
/// <summary>
/// 关闭
/// </summary>
/// <returns></returns>
Task DisconnectAsync(CancellationToken ct);
}
每种通道的读写方式并不一致。比如,几乎每种PLC都会使用一整片连续字节数组来存储数据;而对应扫码枪而言,它很可能基于串口或者USB通信的方式,收取最近一次扫描到的值。
一个测点项目中可以定义1~n个通道(<Channel/>)。
测点(ITag)
在工业通信中,往往有一层非常简单的抽象——用一个寄存器表示一个值。比如用浮点型寄存器表示当前机器人的某个关节轴的角度,或者用一个比特表示外部开关的通断信号。
测点——可观测的点位——是对上面这些表达方式的绝佳抽象。它们往往和硬件寄存器的地址相关,又可以在高层读取或者写入。Itminus.Tags提供了三种层次的测点抽象:
- Tag: 代表单个叶子节点,比如一个数字输入信号、一个数字输出信号、一个16位整型变量、一个32位浮点型变量等等。所有的
Tag都是叶子节点,它们都不能再嵌套子节点。 - TagCbnt:代表一个测点组合(这里Cbnt是Combination的缩写)。有经验的程序员都知道,工业相关程序中
IO往往是瓶颈,减少IO至关重要。我们可以把一些测点进行组合,从底层硬件中整体读取数据、并向底层整体写入,这种整体读写的组合,我们叫TagCbnt(Cbnt是Combination的缩写),TagCnbt的子元素只能是Tag。 - TagGrp: 很多测点往往共享一些特征,比如通道、功能等,这些测点在逻辑上可以划分到一个群组,我们用
TagGrp来表达这个概念(显然,Grp是Group的缩写)。测点群组可以继续嵌套TagGrp、TagCbnt、或者Tag。通常我们会定义 1~n个测点组(TagGrp)作为入口。
测点组可以通过[]索引的方式访问直接子元素。
var child = grp[childName]
这种索引式操作非常类似于字典元素的访问方式——如果对应的元素不存在,它也会报异常。
要访问内部某个子元素下的某个子元素,可以使用多级索引的方式:
var child2 = grp[_path][_to][_child]
此外,我们还可以通过Descendant("path/to/child")方法返回子孙元素:
TagUnion descendant = grp.Descendant("path/to/child")
不过这种.Descendant()方式有两个局限:
- 这里的路径用“/”进行分隔,所以要求路径中的每个片段内都不得含有
/。如果必须要在名称内使用/,只能使用[]逐层索引的方式获取。 - 这里返回的类型是
TagUnion,它代表TagUnion.TagUnit | TagUnion.TagUnit | TagUnion.TagGrp的一种。可以通过.Value属性得到内部的ITag、ITagCbnt、ITagGrp。
为了避免.Descendant()仍需要使用手动类型转化的麻烦,我们提供了三个扩展方法:
ITag tag0 = grp.SelectTag("path/to/child"); // 直接拿到 ITag
ITagCbnt cbnt0 = grp.SelectCbnt("path/to/child"); // 直接拿到 ITagCbnt
ITagGrp grp0 = grp.SelectGrp("path/to/child"); // 直接拿到 ITag
示例:
_grp = tags.SelectGrp("IoBox");
_ledRed = _grp.SelectTag("输出/红灯");
_ledReset = _grp.SelectTag("输出/复位_提示灯");
_btnReset = _grp.SelectTag("输入/复位_执行键");
要从特定的ITag取值,可以直接读取.Value属性——不过为了通用,.Value返回的是object类型。我们可以使用.GetTagValue<TValue>()方法避免手动类型转化:
var resetBnt = _btnReset.GetTagValue<bool>();
var resetLed = this._ledReset.GetTagValue<bool>();
想要写入值,可以直接对属性赋值:
_ledRed.Value = true;
注意: 本类库不会自动尝试任何隐式自动转化,哪怕是你看起来觉得没什么问题的隐式类型转换!这意味着类库使用者不得混写类型,比如不可以把short写给int,也不要把int写给float!
逻辑(ILogicet)
一个项目如果只能对点位进行读写,那大概是只一个无聊的采集应用。
Itminus.Tags 允许你加入自己的业务。在这里,业务逻辑被抽象成简单的ILogicet接口:
public interface ILogicet
{
/// <summary>
/// 运行顺序
/// </summary>
int Order { get; }
/// <summary>
/// 通道
/// </summary>
IReadOnlyList<ITagChannel> Channels { get; }
/// <summary>
/// 测点
/// </summary>
ITagGrp Tags { get; }
/// <summary>
/// 启用?
/// </summary>
bool Enabled { get; }
/// <summary>
/// 是否能匹配入口?返回true表示应该处理当前entry,否则应该跳过处理
/// </summary>
/// <param name="entry"></param>
/// <returns></returns>
bool MatchEntry(ITagGrp entry);
/// <summary>
/// 处理
/// </summary>
/// <returns></returns>
Task ProcessAsync(ITagGrp entry, ITagChannel? thisChannel);
}
你可以在你的程序中以代码的方式注册你的业务逻辑实现,也可以在
xml中以dll的方式手动注册。
针对每个入口(TagGrp),都有一个ITagGrpRunner执行 [扫描->处理 ->输出] loop:
- 扫描: 从底层读取
- 处理: 依次执行各
Logicet的ProcessAsync(entry, thisChannel) - 输出: 向底层刷写
这种机制十分类似于PLC的工作原理。你可能觉得这种机制过于死板、浪费性能、不够酷炫,但是工程项目不是个人花活,想想波音飞机的飞控程序吧,它也以在类似的机制工作着。
我们也提供
.Watch()的事件机制,不过这种事件式的方式并不会改变主循环。作为简介,我们不打算在这里过多阐述这种模式。
项目的构造和运行
通常,我们使用ITagsProjectFactory构建项目,然后运行:
// var dir = // 指定项目运行的目录
// 构造测点项目
var factory = sp.GetRequiredService<ITagsProjectFactory>();
ITagsProject proj = factory.Create(dir, rootXml);
// ... 可选:注册自己的逻辑组件,定制事件钩子等
// 执行 [扫描 -> 处理 -> 输出] 循环
await proj.RunAsync(ct);
如之前我们在起步中所说,这里目录"dir"下通常会有一份测点索引文件(index.xml)。这个索引同样可以拆成三个部分:通道+测点+逻辑,我们将在下一章详细展开如何配置和构建一个项目。