表达式
变量和报警条件背后那一套表达式语言、它可以引用什么、每一种点位的可读面、求值器执行的那些规则、校验给出的提示,以及脚本编辑器和它的“浏览…”选择器怎么用。
Logic 页面上有两处用的是同一套表达式语言:一个表达式变量算出来的东西,以及一个报警所盯着的那个条件。两者都是一条 JavaScript 表达式,在一个够不着这台机器的沙箱里以严格模式求值,也都在同一个脚本编辑器里编辑。动作脚本用的是同一套词汇,但它们是语句块;它们的不同之处在动作里讲。
什么算一条表达式
表达式恰好是一条 JavaScript 表达式:字面量、算术、比较、逻辑运算符、条件运算符 a ? b : c、字符串操作、模板字符串,以及对 JavaScript 内置的 Math、Date 和 JSON 的调用。语句(let、if、循环)属于动作:写第二条语句会被拒绝,写作“一个变量只装一条表达式。语句属于动作。”表达式里任何位置上的赋值以及 ++/-- 都会被拒绝,写作“表达式是只读的。写入属于动作。”
表达式按点位带根的地址来引用它们。每个根交给表达式的是什么:
| 根 | 什么进到表达式里 |
|---|---|
Connector 的点位、Variables、Alarms、一个 Process 单元的 Channels |
一个带下面那套带类型可读面的实时对象。光写一个引用(只写 Connector.Simulated.Pump.Flow)求出来的是这个点位的值;光写一个报警求出来的是它的 IsActive。 |
System、Server、Assets,以及一个 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 毫秒发布它们一次,而编辑器每秒预览草稿两次。