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:代表一个测点组合(这里CbntCombination的缩写)。有经验的程序员都知道,工业相关程序中IO往往是瓶颈,减少IO至关重要。我们可以把一些测点进行组合,从底层硬件中整体读取数据、并向底层整体写入,这种整体读写的组合,我们叫TagCbnt(CbntCombination的缩写),TagCnbt的子元素只能是Tag
  • TagGrp: 很多测点往往共享一些特征,比如通道、功能等,这些测点在逻辑上可以划分到一个群组,我们用TagGrp来表达这个概念(显然,GrpGroup的缩写)。测点群组可以继续嵌套TagGrpTagCbnt、或者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()方式有两个局限:

  1. 这里的路径用“/”进行分隔,所以要求路径中的每个片段内都不得含有/。如果必须要在名称内使用/,只能使用[]逐层索引的方式获取。
  2. 这里返回的类型是TagUnion,它代表 TagUnion.TagUnit | TagUnion.TagUnit | TagUnion.TagGrp的一种。可以通过.Value属性得到内部的ITagITagCbntITagGrp

为了避免.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:

  • 扫描: 从底层读取
  • 处理: 依次执行各LogicetProcessAsync(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)。这个索引同样可以拆成三个部分:通道+测点+逻辑,我们将在下一章详细展开如何配置和构建一个项目。