Chapter 3 配置和构建
我们提供完整的公开接口来让你以编程的方式操作每一个点位和每一个群组——你可以自由地手动调用读写测点(Tbnt/Grp/Unit)功能。但是我们推荐你使用配置的方式描述和运行测点项目。
配置
用代码的方式构建项目的好处是强类型支持和优秀的智能提示,但是纯代码的方式既不够简洁,也不够灵活。我们可以把测点项目中的纯静态结构用一个配置文件加以描述:
<?xml version="1.0" encoding="utf-8"?>
<Project>
<!-- 可以有1~N个通道 -->
<Channel name="S7-1" driver="S7">
<IpAddr>192.168.10.2</IpAddr>
<Rack>0</Rack>
<Slot>1</Slot>
</Channel>
<Channel name="S7-2" driver="S7">
<IpAddr>192.168.10.2</IpAddr>
<Rack>0</Rack>
<Slot>1</Slot>
</Channel>
<!-- 可以有1~N个测点组作为入口 -->
<TagGrp name="信号区-1#" isEntry="true" isEnabled="true" channel="S7-1" scanInterval="10">
<TagCbnt name="PLC" address="DB10.16" access="RO">
<Tag name="心跳" address="DB10.16.0" type="BIT"></Tag>
<Tag name="复位" address="DB10.16.1" type="BIT"></Tag>
<Tag name="故障" address="DB10.18" type="INT16" endian="BigEndian"></Tag>
<!-- ...more... -->
</TagCbnt>
<TagCbnt name="IPC" address="DB10.0" access="RW">
<Tag name="心跳" address="DB10.0.0" type="BIT"></Tag>
<Tag name="复位" address="DB10.0.1" type="BIT"></Tag>
<Tag name="自动" address="DB10.0.2" type="BIT"></Tag>
<Tag name="结束确认" address="DB10.0.3" type="BIT"></Tag>
<!-- ...more... -->
</TagCbnt>
</TagGrp>
<TagGrp name="信号区-2#" isEntry="true" isEnabled="true" channel="S7-1" scanInterval="10">
<!-- ...more... -->
</TagGrp>
<!-- 可以有0~N个逻辑组件 -->
<Logicet>Samples.Plugin1/Samples.Plugin1.dll</Logicet>
<Logicet>Samples.Plugin2/Samples.Plugin2.dll</Logicet>
<!-- ...more... -->
</Project>
这里的有些基本规则:
- 整个
<Project/>可以分成三大部分:<Channel/>(1~N个):描述了项目中用到的通道。<TagGrp/>(1~N个): 描述了项目中用到的测点树。<Logicet/>(0~N个): 描述了项目中用到的业务逻辑(插件)。
<Channel/>:name属性:表示通道的名称,会被测点(以及TagCbnt,TagGrp)所引用。driver属性:表示该通道使用的底层驱动,e.g.S7、COM等。每种驱动的通信方式并不相同。
<TagGrp>:- 通常顶层的
<TagGrp/>会标注isEntry来表明这是一个入口。一个项目可以有多个入口,这些入口会被并行执行。 <TagGrp/>表示一个逻辑分组,其下可以套多层,最终会嵌<TagCbnt/>或者<Tag/>。但是有一些底层驱动不支持批量读写,这时候<TagCbnt/>就是非法的;而另一些底层驱动可能并没有针对单个测点的读写提供支持,这时候直接嵌套<Tag/>也是非法的。总之,使用<TagCbnt/>还是<Tag/>,要查看通道测点的驱动。- 测点的地址是和通道驱动密切相关的,每种驱动的地址都不相同。比如在
S7下DB100.10.0代表的是DB100的第10个字节的第0个比特,特别的,可以用$$10.0代表使用和父级一致的Block;而对于OpcUA,地址则可能是ns=4;s=|var|CODESYS Control Win V3 x64.Application.PLC_PRG.var3。 - 测点的访问方式有
RO|WO|RW|R1W,对应的是 只读 、只写、读写、只在开始时读1次,后续只写,默认是RW。 - 测点组和测点组合上都可以写
isEnabled,用来控制自动轮询是否采集这棵子树(默认true)。它不影响手动读写——细节见 Chapter 2 基本概念。 - 测点的通道检索是冒泡的。即如果本层节点未指定
channel特性,则会向上冒泡检索,直至在祖先节点上检索到非空的通道(或者最终都没有检索到,会给出null)
- 通常顶层的
<Logicet/>: 描述了项目中用到的业务逻辑(插件),程序集里所有 public、非抽象、非泛型的ILogicet实现都会被实例化。- 如果你偏好使用代码的方式注册逻辑,可以不用这里的动态插件机制,直接调用接口
proj.Logicets.Add(...)/TryAddLogicet<T>()。 - AOT(
PublishAot=true)宿主不能使用动态插件:NativeAOT 的运行时没有动态加载程序集的能力,配了<Logicet/>会在加载期直接报错——见 AOT 支持。
- 如果你偏好使用代码的方式注册逻辑,可以不用这里的动态插件机制,直接调用接口
校验和智能提示
书写上面的XML既没有智能提示,也没有自动校验机制。这可能会让不少缺爱的工程师们没有安全感。我们可以引入XSD来解决这个问题,代价则是要使用命名空间,这会显得稍稍繁琐一点:
<Project
xmlns:s7="tags:s7"
xmlns:usbkbcs="tags:usbkeyboardcodescanner"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="..\Schemas\tagsproject.xsd"
xsi:schemaLocation="
tags:s7 your-xsd-loc1
tags:modbustcp your-xsd-loc2
"
>
<!-- 通道,可以配置多个-->
<Channel name="S7-1" driver="S7" >
<s7:IpAddr>172.16.10.20</s7:IpAddr>
<s7:Rack>0</s7:Rack>
<s7:Slot>1</s7: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>
注:这里的相关
XSD文件会在dotnet恢复依赖时自动链接到你的项目中。不过仍需要你在你喜欢的IDE、编辑器中引入我们编写的XSD。
尽管变得啰嗦了许多,但是在这种方式下,IDE、编辑器会自动进行智能提示、补全代码。
需要说明的是,这是XML的编辑时功能,不会影响运行时。如果你还希望在项目运行前也能有自动的XSD程序化校验,则可以builder.EnableXmlSchemaValidation()来启用这个功能:
services.AddTagsProjectServices(builder =>
{
// 启用 XML Schema 校验,每次启动项目之前会自动触发验证。
builder.EnableXmlSchemaValidation();
builder.AddS7Support();
// 增加其它更多的硬件支持,比如:
// builder.AddComScanner();
// ...
});
一旦启用EnableXmlSchemaValidation(),使用未配置命名空间的XML配置、或者非法的XML配置都会在项目运行之前抛出TagsProjectSchemaException异常(它是TagsProjectValidationException的派生类型,二者都属于加载期异常族TagsProjectLoadException;逐条错误在Errors里)。
顺带说明另外 4 个校验器(跨引用、驱动工厂、入口通道独占、嵌套入口)默认就是开启的,不需要(也无法)通过
EnableXmlSchemaValidation()打开——它们只拒绝"确定的配置错误",且因为不依赖XSD,连"无命名空间的老写法"也能正常校验。是否需要运行时XSD校验,由你自己决定。
注:为了保持简洁,让读者能更专注于几个核心概念,在后续本系列文档中,文档本身都使用无
XSD的方式来讲解。但是在我们实际的示例代码仓库中,使用了XSD来保持良好的正确性。这不代表我们在推荐或者不推荐哪一种具体方式。是否需要引入XSD的编辑时或者运行时校验, 由你自己决定。
构建+运行
如果你想手工把上面的index.xml构建成测点项目(ITagsProject),可以使用ITagsProjectFactory接口:
var dir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
// 或者,如果不指定root,将会自动加载`dir/`下的`index.xml`文件
var root = XDocument.Load(xmlFileName).Root;
// 构建测点项目
var proj = factory.Create(dir!, root);
// ... 可选:可以手动注册 Logicets 、可以手动监听事件
在拿到了ITagsProject实例之后,我们可以手动将它运行起来:
// 运行测点项目
await proj.RunAsync(ct);
但是,大部分时候,没必要手动构建再手动运行——直接利用ITagsProjectCtrl接口一键构建+配置+运行:
await projctrl.StartPollAsync(dir, root: null, hook: (proj, sp, ct) =>
{
proj.Logicets.Add(new HeartBeatLogicet(proj));
// ... 其它
return Task.CompletedTask;
});