动作

动作编辑器:标识与位置、脚本块、脚本可以读什么写什么、每一条校验提示、“运行”这条命令和它的日志、上一次运行是谁触发的、什么会运行这个动作、启用和禁用、复制和删除,以及运行的那些上限。

以 Markdown 查看

动作是一段 JavaScript 语句块,它读地址空间,写内部存储值变量和点位。动作是 Logic 的写入通路:它们可以按需运行(运行)、由一个触发器运行、由一个接在它们上面的仪表画面按钮运行,或者在一次 Process 运行记录开始或完成时,经由一条 Logic 动作命令运行。在 Logic 那一栏 Actions 一节下面选中一个动作,或者在它所在文件夹或所属那一节的根的表格里按它那一行上的打开,就能打开它的编辑器(用“新建…”>“新建动作”造出来的动作不会被打开,而是在那张表格里突出显示,见新建与复制);读要查看 Logic,编辑要配置 Logic,运行要操作 Logic。

标题栏显示动作标记、名称、那一行“一段你按需运行、或者由触发器替你运行的脚本,用来写入变量和点位。”,动作关着的时候还有一枚已禁用的胶囊。表单那一列里放着标识、脚本和使用者;右边那条竖栏放着最近一次运行。

标识

字段 它是什么 取值 / 默认值 影响
名称 显示名称,也是一次运行被记进日志时所用的那个标签。 自由文本;新动作叫 ActionN;名字留空则存成 Action。 每一次按键都自动保存。动作是按身份被引用的,所以重命名什么都不用改写,也弄不坏什么。
位置行 这个动作归在哪里,用文字写出来:Actions / Startup / Walk setpoint。 只读。 动作没有地址;这一行是一个位置,绝不是拿去写进表达式的东西。要改就用树里的移动到。
说明(可选) 自由文本。 默认为空。 没有影响。

脚本

脚本卡片就是那个脚本编辑器,带一句提示“JavaScript 语句(let、if、for、函数)。写内部存储值变量用赋值(Variables.Setpoint = 21.5;),写点位用 Connector.Driver.Device.Tag.Write(value)。读取不做质量校验:请先检查 .State(),并在做算术之前用 ?? 兜住缺口。”脚本每一次按键都自动保存,每半秒校验一次;提示出现在那句提示下面。校验从不执行脚本。

脚本就是普通的严格模式 JavaScript(局部变量、if、循环、函数、Math、Date、JSON),跑在和表达式同一个地址空间、同一套可读面之上,只有两处不同。读取不过质量关卡:一个空档读作 null,所以请检查 .State(),并用 ?? 把算术挡住;处理一次不良读数正是动作的用武之地。而且脚本可以读表达式变量(读它们已发布的值);只有表达式不能引用它们。

实时点位(点位、变量、报警、Process 的通道)是点位本身,不是它的值,所以请通过成员读它的值:if (Variables.Start.ValueToBool())、Variables.Level.ValueToReal() > 80、if (Alarms.High.IsActive)。光写在 if、while、do、for、?: 的条件里,作为 !、&&、|| 的操作数,或写在比较、算术(包括 += 和其他复合赋值)里,它要么始终被视为真,要么永远读不到它的值:if (Variables.Start) { … } 不管变量是什么值,每次执行都会执行它的代码块。校验会拒绝这种写法,并说出应该改读哪个成员(校验拒绝什么)。传给函数的点位,以及点位的任何成员,都保持原样。对光写点位做 switch、或 case 写一个点位(请读 .Value),?? 的左边(请读 .Value),以及模板字符串里的占位 ${…}(请读 .ValueToString()),也都一样。对变量的复合赋值(Variables.Total += 1、-=、||= 等)会在写入之前把变量读成点位,所以会被拒绝,并给出能用的普通赋值:Variables.Total = Variables.Total.ValueToReal() + 1。

一个动作可以写什么

恰好有两种写法能改变现场,而且两种都走那条记进日志的写入出口:

写入 形式 目标
内部存储值赋值 Variables.Setpoint = 21.5; 或 Variables.Forno.Setpoint = 21.5; 一个内部存储值变量,文件夹层级也在内。值会被强制转换成这个变量的输出类型,而且在变量是掉电保持时被标记为需要保存。它当场就生效,所以同一段脚本的下一行读到的就是它刚写进去的东西。
点位写入 Connector.Simulated.Pump.Cmd.Write(true); 或 …Cmd.WriteAndWait(true); 一个实时点位。这个值是工程值:它会倒着走完这个点位的转换链,变成设备要的那个原始值,随后出口把它交给那台设备自己的下发队列,欠这台设备的那些值在那里按到达的先后一个一个发出去。

一个点位带着两种写入写法,脚本按自己要不要那个回音来挑:

写法 参数 省掉时 返回 等不等现场设备
Write(value) 那个值。 没有什么可省。 什么都不返回。 不等。值交给设备的队列,下一行就接着跑。
WriteAndWait(value, timeoutMs, continueOnFailure) 那个值、为它等多少毫秒,以及失败之后要不要接着跑。 timeoutMs:改用驱动自己的等待时长。continueOnFailure:true。 只有现场设备收下了这个值才返回 true;其余一切返回 false,被更新的值替换掉也算在内。 等,直到回执到了,或者等待时长用完。

这个等发生在动作通道上,所以一台慢设备拖住的只是排在这一个后面的那些动作,而且它绝不会把写入撤回来:一个没能按时得到确认的值仍在路上,它的回执落地时照样记进日志。答复说的就是这件事,一字不差:“‘Cmd’在 2000 ms 内没有得到确认,该值仍在发往设备的途中。”continueOnFailure 省掉或者写成 true 时,这个写法答 false,脚本接着跑;写成 false 时,运行就在那一行结束,点出目标和原因。

没有任何一次写入自己就能中止一段脚本。 一次在任何东西发出去之前就被拒绝的写入,和一次被现场设备拒绝或者始终没有回音的写入,都只是它自己失败:脚本其余部分照跑,这次运行收尾时算作一次带着失败写入的成功运行。也没有什么会被回滚,所以一次失败的写入之前已经生效的仍然生效,而诊断日志把每一次写入按来源“动作”记成“从旧值到新值”。一次拒绝会用下面这些说法读出来:

  • “对‘X’的写入被驳回:这个值不是有限数。”(NaN 或无穷)
  • “‘Tag’没有在连接器里落地。”(一台已禁用的设备,或者一个 Connector 没有带着的点位)
  • “‘value’无法为‘Tag’完成转换。”
  • “‘X’被‘<holder>’写保护。”(一次配方运行或者一个联锁占着这个信号源)
  • “未写入‘Cmd’:设备未连接。”(这台设备已经知道是趴着的,所以没有给它排任何东西)
  • “对‘Tag’的写入失败:Connector 没有在运行。”
  • “找不到变量‘X’。”以及“‘Variables.X’是表达式变量:它由计算得出,永远不被写入。”
  • 拒绝来自线路那头时,现场设备或者它的驱动答了什么,就照它自己的说法写什么。

一个在设备的队列里被同一点位上更新的写入赶超掉的值不算失败:“‘Cmd’在发送前已被更新的值替换。”说的正是发生过的事,现场设备被告知的是更新的那个值,而这件事除了对一次 WriteAndWait 之外哪儿都不报:它答 false,因为出发的不是这个值。

真正会让一次运行结束的是:脚本自身的错误(语法错误、指不到任何东西的地址、一次异常)、一次写入失败了的 WriteAndWait(…, false),以及运行那一节里的那些上限。

运行本身报得出哪些失败,看的是谁等在那里。一次在任何东西发出去之前就被拒绝的 Write,是在脚本还跑着的时候被拒绝的,所以这次运行把它算进去,记录条也说出来;一次写入在现场设备事后才拒绝的 Write,答复远在运行结束之后才到,所以扛着它的是事件控制台里那个动作名字下面的记录,永远不是这次运行。一次 WriteAndWait 留下来等了答复,所以答复往哪边倒,它都算进去。

校验拒绝什么

提示 原因
“必须填写脚本。” 编辑器是空的。
“脚本错误:…” 这段文字解析不了。
“地址必须是静态的:‘Root’用一个计算出来的地址段访问。” Variables[name] 或者任何非字面量的段。
“地址‘…’不完整。” 一处太短、点不到一个点位的引用。
“没有找到‘X’。” 一次读取,或者一个赋值目标,什么都解析不出来。
“‘X’是一个值:请不加 .Value 直接读取。” 在作为值本身进入脚本的点位上读取实时点位的成员(.Value、.ValueToBool()、.State() 等):System.StartTime.Value,或 Process.Oven_soak.Oven_1.Procedure.Running.ValueToBool()。消息里写的是脚本中用到的那个成员。
“‘X’是点位本身,而不是它的值:作为条件时它始终被视为真。请用 X.ValueToBool() 读取它的值。” 在判断真假的地方光写的实时点位:if、while、do、for、?: 的条件,或 !、&&、\|\| 的操作数(if (Variables.Start))。对报警,消息写的是 .IsActive。
“‘X’是点位本身,而不是它的值:比较或计算永远读不到它的值。请用 X.ValueToReal() 读取它的值。” 在比较或算术里光写的实时点位(Variables.Level > 80、total += Variables.Level)。对 ==、!=、===、!==,消息写的是 .Value;对报警,写的是 .IsActive。对光写点位做 switch,或 case 写一个点位,得到的是带 .Value 的那句消息。
“‘X’是点位本身,而不是它的值:它永远不为空,所以 ?? 总是保留它。请用 X.Value 读取它的值。” 在 ?? 左边光写的实时点位(Variables.Level ?? 0)。对报警,消息写的是 .IsActive。
“‘X’是点位本身,而不是它的值:写进文本里也永远显示不出它的值。请用 X.ValueToString() 读取它的值。” 在模板字符串的占位 ${…} 里光写的实时点位。对报警,消息写的是 .IsActive。
“‘+=’在写入之前把 X 读成点位本身,而不是它的值。请用 X = X.ValueToReal() + 1。” 对变量的复合赋值:+=、-=、*=、/=、%=、**=、位运算的那些、&&=、\|\|= 和 ??=。消息写出所用的运算符,以及按它的意思工作的普通赋值,右边保持原样:算术用 .ValueToReal(),位运算用 .ValueToInteger(),&&= 和 \|\|= 用 .ValueToBool(),??= 用 .Value。
“请直接给变量本身赋值:Variables.Name = value。” = 左边点的是一个成员,或者比一个变量的地址还深。
“‘X’是表达式变量:它由计算得出,永远不被写入。” 给一个表达式变量赋值。
“‘Connector’下的点位是只读的。写点位请用 Connector.Driver.Device.Tag.Write(value)。” 给 Variables 以外的任何根赋值(Process 有它自己的提示)。
“动作里 Process 的点位是只读的;脚本不支持运行命令和预选写入。” 给一个 Process 点位赋值,或者对它调 Write。
“动作里不能调用 Process 的运行命令;命令点位只提供只读的就绪状态值。” 调用 Process.<Model>.<Unit>.Commands.Start() 或者任何别的命令类点位;一个 Commands 点位只读得出这条命令此刻能不能执行。
“点位不支持 ++ 和 -- 运算符。请用 Variables.Name = Variables.Name.ValueToReal() + 1。” Variables.N++。
“Write(value) 就是点位写入:Connector.Driver.Device.Tag.Write(value)。” 对一个不是点位地址的东西调了 Write。这句话点的是当时用的那个写法的名字,所以 WriteAndWait 得到的是同一句,两处都换成它自己的名字。
“WriteAndWait 的等待时间必须是毫秒数:WriteAndWait(value, 2000)。” 第二个参数写出来了,而且不是一个数。
“WriteAndWait 的失败后继续必须是 true 或 false:WriteAndWait(value, 2000, false)。” 第三个参数写出来了,而且不是 true 也不是 false。
“WriteAndWait 最多接受三个参数:值、以毫秒计的等待时间,以及失败后是否继续。” 出现了第四个参数。

WriteAndWait 的这三句判的是文字本身写了什么。一个编辑器光靠读读不出结论的参数(一个变量、一个算出来的表达式),要等这次调用真的发生时才判,那时同样这句话会让运行在那一行结束。

JavaScript 的局部变量(let x = 1; x = x + 1;)不受这些规则约束。

运行

运行是一个打开着的动作上那条命令栏里排在最前的命令;它只对拥有操作 Logic 的角色画出来,工作站只读时带着“运行时已停止”置灰(工程锁定不会把它置灰:运行是操作,不是配置)。角色同时还拥有“配置 Logic”时,编辑器里的草稿会先保存下来,运行执行的是保存之后的那份定义;没有“配置”时,运行执行的是已经落盘的那份定义(那些字段本来就是只读的,所以两者绝不会不一样)。

每一次运行都在动作通道上、用一个全新的脚本引擎执行,所以两次运行之间不会有状态漏过去,两段脚本也绝不会同时跑:它们一次一个,按被要求的先后跑。这条通道不是那 100 毫秒的扫描周期。一段脚本跑着的时候,表达式、报警、确认以及从别处来的写入都照常进行,所以一次扫描周期可能落在一段脚本中间,同一段脚本里的两次读取也可能看到不同的周期;换来的是,一个停下来等设备的动作只耽误排在它后面的那些动作,工作站里别的什么都不耽误。

一次运行超过 500 毫秒(“脚本已中止:它超过了 500 ms 的时间上限(是不是死循环?)。”)、100000 条语句或者 4 MB 内存就被中止,每一种都有一条点名那个上限的提示。那 500 毫秒量的是脚本在跑的时间:一次 WriteAndWait 在等设备的整段时间里计数是停着的,等完再接着算,所以一段正确的脚本绝不会为慢设备背锅。已禁用的动作根本不跑就失败(“该动作已禁用。”);运行时已停止时答的是“逻辑运行时没有在运行。”

每一次写入都过去了时,记录条报告“运行正常,用时 12 ms。”;脚本自己跑完了而有写入没成时,报告“运行正常,用时 12 ms,1 次写入失败:Cmd:未写入‘Cmd’:设备未连接。”(从第二次失败起是“运行正常,用时 12 ms,3 次写入失败,第一次:Cmd:未写入‘Cmd’:设备未连接。”);运行本身收场不好时,报告“运行失败:<reason>”。

失败的那次运行会到事件控制台,记在这个动作的名字下面,并连同是谁发起的:命令按的是“手动”,仪表画面按钮是 Dashboard,代理是 MCP,排程运行的是那个触发器的名字,Process 运行记录是 Process · <unit> · <action>。它第一次被记为一条警告,在它以同一个原因反复失败期间被汇总,恢复时再记一条,而那一条恢复的记录,是一次成功的运行在那里唯一写下的东西。一次被拒绝的写入也照同样的路、记在同样的名字下面到达控制台,而被点名的是那个动作、不是那次运行,道理是一样的:现场设备的答复可能在运行结束好几秒之后才到,那时已经没有哪次运行还能失败了。单纯成功的一次运行,以及它写下的每一个值,只写进详细捕获:一个每次扫描周期都写的脚本会产出成千上万个值,而一个被它们填满的控制台,是一个谁也读不到工作站要闻的控制台。

最近一次运行那条竖栏显示这个动作上一次是什么时候跑的(或者“从未运行。”)、“用时:N 毫秒”、是谁触发的(见下文)、放在一条提醒里的最近一次错误、单独放在一条提醒里的那次运行写失败的那些写入(和记录条上带的是同一句话,所以一次成功了却仍然把某个设定值留在原处的运行,在这里也照样说出来),以及那句提示“运行都在动作通道上执行,一次一个。只有失败的运行才会进入事件控制台;成功的运行以及它写入的每一个值,都留给详细捕获。触发器按计划调用动作。”这条竖栏属于活着的那个动作:一次自动保存绝不会把它抹空;把运行时重启一次会。

触发来源

在用时下面,“触发来源:…”这一行说明最近一次是什么运行了这个动作:

是什么运行了它 这一行写着
本页上的运行 “触发来源:在本页运行”
仪表画面上的一个按钮 “触发来源:仪表画面上的按钮”
代理(action_run) “触发来源:代理”
一个触发器 触发器的名称:“触发来源:Heater off when too hot”。
一次 Process 运行记录里的一条 Logic 动作命令 单元和动作组:“触发来源:Process · Oven 1 · After soak”。

使用者

使用者卡片列出工作站上运行这个动作的一切(“工作站上运行这个动作的一切。”),在打开动作时读取:触发它的每一条 Process 命令、绑在它上面的每一个触发器、运行它的每一个仪表画面按钮,各占一行。

种类 位置 名称
Process 命令 模型。 动作组和时机:After soak · 完成。
触发器 空。 触发器,放在它的文件夹下面。
按钮 仪表画面。 按钮上的文字。

什么都没有时,它写“工作站上没有任何东西运行这个动作。”;引用读不出来时,写“这个动作可能仍在使用中。删除或重命名它之前,请再试一次。”并带一个重试。这张卡片只读,所以每个角色都看得到,工作站被工程锁定时也一样。停用或删除一个动作之前先读它:一条动作已经不在的 Process 命令什么都不运行,并在它自己的页面上说明。

读取触发它的那次运行记录

一次 Process 运行记录在完成时触发的动作,从单元的 LastRun 分支读取那次运行记录,动作运行时这一支已经装着它;开始时触发的动作,从 Procedure.Metadata 读取正在开始的那次运行记录的元数据。Process 的点位读作普通值,后面不跟 .Value,这一点和变量不同;运行记录里没有的点位读作 null,所以要先判断:

if (Process.Oven_soak.Oven_1.LastRun.Verdict === "Ok") {
  Variables.Last_serial = Process.Oven_soak.Oven_1.LastRun.Metadata.Serial;
}
const peak = Process.Oven_soak.Oven_1.LastRun.Evaluations.Soak_check.Temperature_in_range.MaxValue;
if (peak !== null) {
  Variables.Last_peak = peak;
}

地址要整段写出来,和脚本里的每个地址一样:地址不能用一个变量拼出来。

命令

命令 作用 何时置灰 何时不画出来
运行 保存草稿(有“配置 Logic”时)并把一次运行排进队列。 运行时已停止。 角色没有“操作 Logic”。
启用 / 禁用 翻转“已启用”;已禁用的动作拒绝运行,并让绑在它上面的每一个触发器空转,触发它的 Process 命令每次都被拒绝,并显示需要复核。 工作站被锁住或只读。 角色没有“配置 Logic”。
复制 把这个动作复制到 Actions 根下面一个空闲的名字上,副本不会被打开,而是在这个根的表格里突出显示(见新建与复制);副本没有运行记录,也没有触发器绑在它上面。 工作站被锁住或只读。 角色没有“配置 Logic”。
删除动作 先问“要删除‘X’吗?这会删除该动作。 此操作无法撤销。”绑在它上面的触发器会变成没有绑定,而不是跟着一起被删掉;运行它的 Process 命令会留下来,什么都不运行,并显示需要复核。 工作站被锁住或只读。 角色没有“配置 Logic”。

Actions 根的面板在“数值”那一列里列出每一个动作和它的状态:“已启用 · 上次运行 <when>”,上一次运行失败时加“· 错误”,运行成功了而它的某次写入没成时加“· 写入失败”,否则是“已禁用”。

动作不做什么

它们不下发 Process 的运行命令、不预选一个流程、也不给运行记录加备注(那些点位是只读的就绪值和预选值);它们不确认报警;它们不并行运行;它们两次运行之间不留状态,也不共用函数;而且它们不由数值变化或条件触发,只由“运行”、一个触发器的定时间隔、一个仪表画面按钮、代理或者一次 Process 运行记录的命令触发。Process 运行记录从不等它触发的动作,动作的结果也从不写进运行记录。