
导读
前文主要是从代理点的角度看“值传递”。而点(BControlPoint)是由 slot 组成的,那么 set 作为一个 Action,作用对象是 fallback,我们从 fallback 属性出发,它的值变化,会有三个方向的传递:
本文我们从代码层面再梳理一遍 set 操作后三个方向都执行了哪些过程。
控制逻辑回顾
两个代理点,Point1 来自 Modbus 设备,Point2 来自 BACnet 设备,这两个点做与运算,将结果输出到 Point3(软点)的 in16 上,同时也链接到 Point4 (软点)的 set 操作。现在,对 Point1 进行 set 操作,将当前值由 false 改为 true,下面我们从代码层面,来看命运的齿轮是如何转动的。
本文我们省略了一个 Niagara 平台本身更深的底层机制,那就是 set 操作是从哪里触发的?它可以从 workbench 触发,也可以从浏览器触发,总之都是用户通过 UI 操作启动的,但无论是哪一种,都需要一个后台机制将 UI 的上的 set 操作同步到 station 的代理点之上。这部分是 Niagara 底层机制的一个重要特征,但不是本文重点,在此略过。参考:你知道吗?Niagara其实是个软件集合
Set() 方法中,先通知代理扩展有 set 操作(Action) 被触发(方向①),然后修改 fallback 属性及其后续在方向②和③上的影响。
01
方向① — BProxyExt,“两个异步,层层转包”
notifyProxyExtForActionInvoked() 是去往代理扩展方向的入口,这个方向上会经历两个“异步”:
1.1 第一个异步
notifyProxyExtForActionInvoked() 调用了 BProxyExt 类的 forceWrite() 方法。虽然从名字上看它是个写操作,其实它只是设置了“待写”的标志位。
后续在 BControlPoint 对象的异步方法 doExecute() 中会调用 BProxyExt 的 onExecute() 方法,这里会检查这个“待写”标志位,然后根据当前对象的同步策略(Tuning)去安排写命令。
BProxyExt 的 onExecute() 方法:检查待写标志,如果有,就通过当前代理的同步策略(Tuning)去安排写操作。同步策略负责处理诸如 minWriteTime,maxWriteTime 和 staleTime 等相关参数和实际写操作之间的关系。
1.2 第二个异步
同步策略 Tuning 负责的这些事情,对所有驱动都是一样的,但是每个协议中具体的写命令是什么,那要在这种协议的驱动中去具体实现。抛开某种协议的具体命令格式不谈,在 Niagara 上有两种驱动框架,一种叫做 basic driver,另一种叫做 N driver。无论哪种,对 Tuning 的接口都一样,就是在 BProxyExt 中实现 write() 方法。都是通过这个方法,将写命令传递给 Niagara Driver Framework 的发送消息队列,这个交由底层驱动处理收发消息就是第二个异步。
如果要再往下挖的话,其实还有第三个异步:因为 Driver Framework 和操作系统之间还有输入输出流的转发,操作系统可并不只是为 Niagara 进程服务的,可能还有其他进程在做发送和接收。这就超出Niagara 范围之外了。
什么时候用同步,什么用异步?
吃煎饼果子的时候需要同步,订制大褂儿需要异步。
为什么煎饼果子就需要同步?
第一,煎饼果子,你肯定得吃刚出锅的吧(需要在第一时间获得结果);
第二,它加工时间很短,就算前面有俩人,等五分钟也就吃上了(等待时间不长)。
而如果你去订制大褂,你会一直在那等吗?显然不可能,因为得做好几天,这时候你就只能选择异步 —— 量完尺寸,选好面料就回家了,等裁缝做好了,通知你再来取。
02
方向①的路径 — execute,“两次刷新,由内而外”
set() 方法中,通知完代理扩展后,接着就修改了 fallback 属性,之后调用了一个 setValueValue() 方法,这个方法中会调用 BComponentSlotMap 类中的一个重要方法:modified() ,它就是方向②和方向③的分支:
2.1 第一次“刷新”
当前例子中,由于 fallback 下游并没有任何 BLink 连接,所以此处方向③上不会发生任何事情。接下来 fireComponentEvent() 会通知当前点(某个属性)发生了“变化”,也就是 fwChanged() 方法,这个方法中会调用 BControlPoint 中至关重要的一个方法 ——execute(): “刷新” 。
关于“刷新”—— 异步方法 execute
execute 方法是所有“点”对象中,至关重要的一个方法,对于我们例子中的 Point1 来说,它是一个 BBooleanWritable, in1 ~ in16 + fallback 任何输入属性发生变化,都可能会引起 out 值的变化。那么根据 in1 ~ in16 + fallback 计算 out 这个“刷新”过程,就是在 execute() 中实现的。可写点还有2个保护参数叫做 minActiveTime 和 minInactiveTime,out 值是否受这两个保护参数制约,也是在这个“刷新”过程中检查的。除此之外,前面讲方向1的时候我们也提到了(第一个异步),它还会检查“待写”标志位,从而决定是不是通知代理扩展去执行写操作,就是在 executeExtensions() 中实现的。
从 slot 视图可以看到,它是一个隐藏的、异步的 Action:flags = ha。
2.2 第二次“刷新”
第一次刷新中的 fwChanged() 方法 invoke 这个 execute(),并不是立即刷新的,它是通过 engine manager 线程在后台执行的。第一次刷新的结果是,fallback 的变化导致了 out 的变化,而 out 也是一个属性,它的改变,又会触发一次 modified(),从而带来了第二次刷新。
out 值的改变带我们再次来到②和③的路口 modified() 方法中,这次 out 的下游有一个 BLink 连接到了 And 模块。这时,它就要通过 BLink 向下游传递值改变了,这个就是方向③。
03
方向③的路径 — BLink,“两个节点,多米诺骨牌效应”
BLink 是实现事件在不同对象之间传递的关键。Niagara 是基于组件编程的(Component Oriented Programming),那么组件之间能够连接在一起形成一个联动的整体成为完整的控制逻辑就是借助“连接”(BLink) 。BLink 就像是机械机构中,齿轮组件之间的传动皮带或者齿条。
BLink 本质上是创建了两个 Slot 之间的事件传送通道,它被存储在 target component 上。既然是通道,那必然存在两端:进口和出口。进口连接着事件的“源头”,作为源头,是将事件塞入管道发送出去,发出事件对源头对象没有任何影响。而出口连接的下游端口是目的对象,目的对象收到事件后要做出适当的响应,事件对目的对象产生影响。
连接本身还有状态:激活态 (activated)和 注销态(deactivated )。activate 之后,连接会在源组件(source component)上创建一个反向索引(knob),source component 通过这个 knob 就可以找到 target slot。
BLink 是存储在 target 上,通过它可以找到 source component;knob 是存储在 source component 上,通过它可以找到 target。
关于 BLink 的内容稍微有点多,为避免占用本篇的过多篇幅,以后通过专题再单独讲讲 BLink。
由于改变都是从 source component 向 target component 传递的,那么回到 source component 中方向②和方向③的路口,也就是属性改变之后触发的 modified() 方法:
在这个例子中,out 属性值改变之后,需要通过 out 属性对应的 knob(存储在 source component 上),将值和状态推送给 And 组件的 inB 属性。随后改变影响到 target slot,也就是 And 组件的 inB 发生变化 —— 属性改变会触发方向②的变化,通过 execute() 方法刷新 out 的属性,out 属性变化又会将事件从 And 的 out 推送到 Point3 的 in16 和 Point4 的 set …… 这样事件就一级一级通过 BLink 传递下去,形成了“多米诺骨牌效应”。

