表达式

变量和报警条件背后那一套表达式语言、它可以引用什么、每一种点位的可读面、求值器执行的那些规则、校验给出的提示,以及脚本编辑器和它的“浏览…”选择器怎么用。

以 Markdown 查看

Logic 页面上有两处用的是同一套表达式语言:一个表达式变量算出来的东西,以及一个报警所盯着的那个条件。两者都是一条 JavaScript 表达式,在一个够不着这台机器的沙箱里以严格模式求值,也都在同一个脚本编辑器里编辑。动作脚本用的是同一套词汇,但它们是语句块;它们的不同之处在动作里讲。

什么算一条表达式

表达式恰好是一条 JavaScript 表达式:字面量、算术、比较、逻辑运算符、条件运算符 a ? b : c、字符串操作、模板字符串,以及对 JavaScript 内置的 MathDateJSON 的调用。语句(letif、循环)属于动作:写第二条语句会被拒绝,写作“一个变量只装一条表达式。语句属于动作。”表达式里任何位置上的赋值以及 ++/-- 都会被拒绝,写作“表达式是只读的。写入属于动作。”

表达式按点位带根的地址来引用它们。每个根交给表达式的是什么:

什么进到表达式里
Connector 的点位、VariablesAlarms、一个 Process 单元的 Channels 一个带下面那套带类型可读面的实时对象。光写一个引用(只写 Connector.Simulated.Pump.Flow)求出来的是这个点位的值;光写一个报警求出来的是它的 IsActive
SystemServerAssets,以及一个 Process 单元的运行状态、产量、命令和预选 当前那个值本身:一个数字、一段文本、一个布尔量或者一个时刻。直接用就行;没有成员要接。

System.Now 是这次求值的时刻:一次求值里对它的每一处引用读到的是同一个时间。它和每一个时间戳一样,以 JavaScript 的 Date 进来,所以时刻之间自然能比较和相减:(System.Now - Connector.Simulated.Pump.Run.SourceTimeStamp()) / 1000 得到的是秒。

表达式变量可以引用点位、内部存储值变量、报警、Process 的点位、Assets 的点位以及 System 和 Server 的诊断值,但绝不能引用另一个表达式变量:编辑器会拒绝它(“‘Variables.X’是一个表达式变量。表达式不能引用其他表达式变量;请改为引用内部存储值变量或底层的点位。”),这样结果就永远不取决于求值顺序。报警条件除了 Alarms.* 之外什么都可以引用(“报警条件不能引用其他报警。”),而且必须产出一个布尔量。

一个点位的可读面

一个实时点位提供:

成员 返回 含义
.Value 带类型的值 工程值,走完这个点位整条转换链之后的样子
.ValueRaw 带类型的值 规整到点位声明类型的原始值,在量程换算之前
.ValueObject 任何东西 驱动产出来的、原封不动的那个值
.ValueScaled.ValueCalibrated.ValueLimited.ValueRounded 数字 分别走完量程换算、校准、限值和小数位环节之后的值
.ValueToBool().ValueToInteger().ValueToReal().ValueToString().ValueToDateTime() 转换后的值 工程值转换成那个类别;没有值仍然是没有值
.State() 布尔量 质量:读数良好时为 true
.SourceTimeStamp() Date 设备产出这次读数的时刻
.ApplicationTimeStamp() Date 内置服务器收到它的时刻
.Name.EngineeringUnit 文本 这个点位的名称和单位

变量提供 .Value、五个 .ValueTo*() 转换、.State().ApplicationTimeStamp().Name.EngineeringUnit。报警提供 .IsActive.IsAcked.CurrentState.State()(它的条件在上一次扫描周期里求不求得出来)、.ActiveSince().LastTransitionAt().Name。Process 通道在绑到一个点位上时提供点位那套面,在绑到一个变量上时提供变量那套面;没有绑定的通道什么都不提供。实时对象上别的东西都够不着:这套面之外的成员读作 undefined,把它当函数调用会在校验时失败。“浏览…”选择器在每个点位下面列的就是这些成员,都不用凭记忆手打。

求值器执行的那些规则

在编辑器里随着你打字执行一遍,在每 100 毫秒的扫描周期上再执行一遍:

  • 质量关卡。 求值之前,每一个被引用的点位都被解析并刷新。只要有任何点位缺失、不可用或者没有值,结果就直接成为一个质量不可用的空档,表达式根本不跑,所以一台离线的设备绝不可能造出一次采样,null 也绝不会进到引擎里变成 0
  • 不是有限数的结果是空档。 NaN 或无穷(除以零、未定义的算术)绝不会变成一次采样。
  • 失控保护。 一次求值超过 250 毫秒、5000 条语句或者 4 MB 脚本内存就被中止,并算作一个质量不可用的空档。每一段表达式文字各留一个引擎,所以什么都不会从另一条表达式那里漏过来。
  • 强制转换。 求成功的结果会被强制转换成这个变量的输出类型;报警条件被强制转换成布尔量,而转换不了的结果让报警保持原状态。
  • 错误是空档,不是异常。 扫描周期里的一次运行时错误产出一个空档和一条诊断日志(第一次失败记一条,重复期间记汇总,恢复时记一行);扫描周期接着往下一个变量走。

校验拒绝什么

编辑器每半秒校验一次草稿,并在数值或条件下面显示第一个问题:

提示 原因
需要填一条表达式。 / 需要填一个条件。 编辑器是空的。
表达式错误:… 这段文字解析不了。
一个变量只装一条表达式。语句属于动作。 超过一条语句,或者一条不是表达式的语句。
表达式是只读的。写入属于动作。 一次赋值或者 ++/--
地址必须是静态的:访问‘Root’时用了算出来的段。 Variables[name] 或者任何非字面量的段。
地址‘Connector.Simulated’不完整。 这条链太短,在它的根下面点不到一个点位。
找不到‘X’。 这个地址什么都解析不出来。
‘X’是一个表达式变量。… 引用了另一个表达式变量。
报警条件不能引用其他报警。 条件里有一处 Alarms.* 引用。
条件必须求出一个布尔值,但它产出了‘x’。 条件的试算产出了一个存在、却转换不成布尔量的值。
表达式错误:实时点位不能在这里直接使用。请通过它的 .Value 成员来读(例如 Connector.Driver.Device.Tag.Value)。 在需要基本类型的地方用了一个实时对象(Tag + 1)。
表达式错误:<engine message> 试算抛了异常:一个不认识的标识符,或者一个被挡住的成员被当作函数调用。

试算是拿当前的值来跑的,不管质量如何,所以一条提示反映的是这条表达式,不是一个不可用的输入;不可用的输入改在引用的点位卡片里显示。

脚本编辑器

页面上每一条表达式、每一个条件和每一段脚本都在同一张卡片里编辑:一行小标题(表达式、条件或脚本)、一个浏览…按钮、编辑器、一句提示和那条校验提示。编辑器是带 JavaScript 高亮的 CodeMirror,有撤销历史,也有地址记号的自动补全:光秃秃的一个记号补根名,然后补已经打出来的那条路径下面的子项,等这个记号点到一个点位之后,成员也在内(见输入一个地址)。这段文字去抖之后推给草稿并自动保存。表达式和条件那两个框至少 56 像素高,动作脚本那个框 160 像素。编辑器引擎起不来时,这个框降级成一个纯文本区域,仍然读得写得,只是没有高亮、没有补全、也没有历史;这时“浏览…”把挑中的地址接在末尾,而不是放到光标处。

浏览…打开一个对话框(“浏览:插入到表达式”),里面是整个地址空间成员那一层的树;选中一行按插入,或者双击它,就把它的地址放到光标处。每个编辑器各有自己的对话框,所以同一页面上的两个编辑器绝不会共用一个选中项。

表达式不做什么

它们什么都不写,不调 Process 的运行命令,两次求值之间不留状态,不能定义在多条表达式之间共用的函数,也不会按需求值:扫描周期每 100 毫秒发布它们一次,而编辑器每秒预览草稿两次。