DLMS/COSEM 蓝皮书解读(九):Script table 类(class_id = 9)—— “到点了做什么”:把一串 SET / ACTION 打包成可执行脚本
系列说明:本系列基于 DLMS UA《Blue Book(蓝皮书)第 16 版 · 第 2 部分 —— COSEM interface classes》,一个接口类一篇。第 8 篇讲了
Clock(class_id = 8),它解决的是"全表的时间基准从哪来"。本篇的Script table解决的是紧接着的下一个问题:有了时间基准之后,"到点了具体要执行哪些动作"由谁来描述、由谁来执行。它是Activity calendar(20)、Schedule(10)乃至Register monitor(21)共同的"执行器"。
上篇回顾:
Clock(OBIS 典型为0-0:1.0.0)把"现在几点"做成了一个可配置、可信、可校时的对象,于是Activity calendar能判断当前处于哪个费率时段、Profile generic能给曲线打上时间戳。但Clock只回答"何时",它不回答"到 22:00 该做些什么"。如果"到点做什么"只能靠主站在那一刻远程下发一串SET/ACTION,那么通信中断、下发顺序错乱、成千上万块表无法在同一秒内全部下发,都会让表内状态停在半途 —— 数据本身没错,动作却漏了。
(版本说明:Script table在蓝皮书里只有version = 0一个版本,raw 原文未给出其他 version,故本篇不设版本对比章节。)
0. 为什么必须要有 Script table
把"表内自动化"这件事拆开看,一共只有三个要素:什么时候(when)、做什么(what)、谁去执行(who)。COSEM 的分工异常清楚:
| 要素 | 由谁负责 | 类 |
|---|---|---|
| 现在几点 | Clock | class_id = 8 |
| 什么时候执行 | Activity calendar/Schedule/Single action schedule | 20 / 10 / 22 |
| 做什么 | Script table | class_id = 9 |
| 被操作的对象 | Register activation、Register、Demand register、Profile generic… | 6 / 3 / 5 / 7 |
先看几个真实的"到点动作":
| 业务场景 | 到点要做的一串动作 |
|---|---|
| 22:00 切谷费率 | SETRegister activation的active_mask= “valley” |
| 每月 1 日 00:00 结算 | ACTIONProfile generic的capture抓一次冻结 |
| 负荷超阈值 | SET告警标志 + 触发一次上报 |
不用Script table的话只有三条路,每条都带硬伤:① 主站到点下发—— 依赖在线率,一个含 5 个SET的脚本若在第 3 个断线,表内状态就停在半途,5000 块表逐个下发还会有几十秒时间离散;② 表内固件写死—— 改一次时段就要升级固件,在计量行业意味着重新送检;③ 厂商私有对象—— 各家各一套,主站换一家表就得重写,互操作性归零。
COSEM 的答案是把"做什么"外置成数据:动作清单写在一个attribute(scripts)里,由通用执行器(execute方法)执行。改动作 = 改数据(一次SET),不需要动固件,也不会因为外部通信而中断。
蓝皮书原文(Script table, Overview):
“This IC allows modelling the triggering of a series of actions by executing scripts using the execute (data) method.”
“Script table objects contain a table of script entries. Each entry consists of a script identifier and a series of action specifications. An action specification activates a method or modifies an attribute of a COSEM object within the logical device.”
两句话拆出三个要点:① 一个脚本(script entry)= 脚本标识符 + 一串动作规格(action specification);② 单条动作只有两种可能 —— 激活一个 method,或修改一个 attribute;③ 被操作的对象必须在本 logical device 内(脚本不能跨设备操作别的表)。
一句话定位:
Script table=表内可配置的宏(macro)。它自己不计时、不判断条件,只负责"被叫到时,把这串动作按顺序做完"。
1. 类蓝图
Script table 0...n class_id = 9, version = 0基数是0...n—— 一台设备里可以有多个Script table实例(按用途分组:费率切换一张、日冻结一张、告警一张),也可以一个都没有(有些简表型表计没有任何时间表驱动的行为)。
| 属性 | 静态/动态 | 数据类型 | Min | Max | Def | Short name |
|---|---|---|---|---|---|---|
logical_name | (static) | octet-string | x | |||
scripts | (static) | array | x + 0x08 |
| 方法 | 必选/可选(m/o) | Short name |
|---|---|---|
execute (data) | m | x + 0x20 |
注 1:原文未给 Min / Max / Def 三列任何取值,故上表留空。另外两个属性全是 static—— 在 COSEM 里 static 表示"值不会由设备自发改变",不等于不可写,
scripts恰恰是主站最常改写的属性(改脚本 = 一次SET)。
注 2:关于executed(执行结果记录)——本卷 raw 原文未提供该项。Script table(version = 0)只有上表 2 个属性,没有记录"哪个脚本在何时被执行、结果如何"的属性。如果你的设备对象列表里出现了该类的 attribute 3,请先核对这块表所依据的蓝皮书版本与厂商文档,不要按 version = 0 的模型去解析。
三个必须记住的结构性事实:
execute是m(mandatory,强制实现)—— 对比Clock的 6 个校时方法全是o:本类存在的全部意义就是这个方法,没有它这个类毫无价值。- 属性区与方法区的 Short name 不连续:属性结束在
x + 0x08,方法却在x + 0x20。SN(short name)寻址时不能按 0x08 步长顺推。 - 只有 2 个属性,但
scripts是三层嵌套结构(array → structure → array → structure),实际复杂度全藏在这一个属性里。
2. 属性逐条解读
2.1logical_name(static, octet-string)
“Identifies the ‘Script table’ object instance.”
6 字节OBIS。Script table常见的 OBIS 取值形如0-0:10.0.0(示例值,非蓝皮书原文,实际以设备对象列表为准)。注意 A 组 = 0,属于"抽象/公用"对象。
这个 OBIS 会被别的对象引用(见示例 4):Activity calendar的script_logical_name、Schedule的script_logical_name、Single action schedule的executed_script,填的都是它。因此改一个Script table的 OBIS 会影响所有引用方。
2.2scripts(static, array)—— 本篇的全部内容
“Specifies the different scripts, i.e. the lists of actions.”
原文给出的类型定义是两层:
scripts ::= array script script ::= structure { script_identifier: long-unsigned, actions: array action_specification }即:scripts是脚本的数组,每个脚本是一个2 元素 structure—— 一个long-unsigned的编号(script_identifier)+ 一个动作数组(actions)。
关于编号有一条硬规则:
“The script_identifier 0 is reserved. If specified with an execute method, it results in a null script (no actions to perform).”
script_identifier = 0是保留值,拿它去execute得到的是"空脚本(null script)"—— 什么都不做,而且不报错(见示例 3)。
action_specification:一条动作怎么写
action_specification ::= structure { service_id: enum, class_id: long-unsigned, logical_name: octet-string, index: integer, parameter: service specific }五个字段的分工,用一句话概括:“对哪个对象的什么东西,做什么,带什么参数”。
| 字段 | 类型 | 含义 |
|---|---|---|
service_id | enum | 做什么:写属性(write attribute)/ 执行方法(execute specific method) |
class_id | long-unsigned | 目标对象的类(6 =Register activation、5 =Demand register、7 =Profile generic…) |
logical_name | octet-string | 目标对象的 OBIS(不是Script table自己的!) |
index | integer | 属性索引 / 方法索引 |
parameter | service specific | 写属性时 = 要写入的值;执行方法时 = 该方法的data |
原文对service_id与index的说明是:
“the service_id element defines which action to be applied to the referenced object: write attribute, execute specific method”
“the index element defines (with service_id 1) which attribute of the selected object is affected; or (with service_id 2) which specific method is to be executed. The first attribute (logical_name) has index 1, the first specific method has index 1 as well.”
推导出的枚举取值与两条编号规则:
| service_id | 含义 | index的含义 |
|---|---|---|
| 1 | write attribute(SET) | attribute 索引,从 1 开始,logical_name是 1 |
| 2 | execute specific method(ACTION) | method 索引,也从 1 开始,第一个 specific method 是 1 |
说明:raw 原文此处只写了 “write attribute / execute specific method” 两个枚举名,数值 1 / 2 是从上面那句
with service_id 1/with service_id 2推定出来的(原文此处枚举数值在提取时丢失)。工程上的对应关系即 1 =SET、2 =ACTION。
"index 从 1 开始、attribute 与 method 各有一套编号"是最容易写错的地方:想写Register activation的active_mask(第 4 个属性),index就是4;想执行Profile generic的第 2 个方法capture,index是2—— 两者互不相干,不要混着数。
两条 NOTE 同样关键:
“NOTE 1 — The action_specification is limited to activate methods that do not produce any response (from the server to the client).”
脚本只能调用"无响应数据"的方法。也就是说Profile generic的capture(method 2)可以,get_buffer_by_range(有返回数据)就不行 —— 脚本执行发生在表内,没有客户端在等这个返回值。
“NOTE 2 — A ‘dummy’ action specification with all elements 0 means that the action is not configured.”
所有元素全 0 的 action_specification = 该动作未配置(不是"执行一次空操作")。这是标准的占位写法,见示例 3。
3. 方法:execute (data)
execute (data) data ::= long-unsigned“Executes the script specified in parameter data.”
“If data matches one of the script_identifiers in the script table, then the corresponding action_specification is executed.”
三条工程要点:
data是script_identifier,不是数组下标。它按"编号"去scripts里匹配,匹配上才执行。写成下标极可能命中另一个脚本(编号恰好等于下标时"看起来是对的",改了配置才发现错位),匹配不上则什么都不执行。- 匹配不上时原文没有规定任何报错语义—— 既没有说返回错误,也没有说忽略。工程上必须自己确认:主站触发后应读回受影响对象的值做校验,不要假设"没报错就执行了"。
- 谁可以调用它?原文明确:
“A certain script may be activated by other COSEM objects within the same logical device or from the outside.”
即表内对象(Activity calendar/Schedule/Single action schedule/Register monitor)或外部客户端(主站发一条ACTION)都可以触发。这一句也是安全设计的依据:execute是一条远程执行通道,必须放在高安全等级后面。
同一时刻撞车的处理规则:
“If two scripts have to be executed at the same time instance, then the one with the smaller index is executed first.”
索引较小者先执行。注意这里说的是脚本在scripts数组中的索引(Schedule里对 entry 也有同样的"低 index 优先"规则)。跨对象(两张Script table之间、Script table与Single action schedule之间)的顺序,原文未作规定,不要依赖。
4. 【实战举例】
以下示例中的 OBIS、配置取值与报文字节均为帮助理解而构造,非蓝皮书原文,实际以设备对象列表与厂商文档为准。
示例 1:一条最经典的脚本 —— 22:00 切谷费率
需求:每天 22:00,把Register activation(class_id = 6)的active_mask改成 “valley”。
Script table logical_name = 0-0:10.0.0 (示例) scripts[0] = { script_identifier = 0x0001, actions = [ { service_id = 1, # write attribute (SET) class_id = 6, # Register activation logical_name = 0-0:10.0.2, # 目标对象(示例) index = 4, # attribute 4 = active_mask parameter = "valley" } # octet-string ] }SET 该scripts属性的 A-XDR 编码(示例):
01 01 array(1) scripts 02 02 structure(2) script 12 00 01 long-unsigned 0x0001 script_identifier 01 01 array(1) actions 02 05 structure(5) action_specification 16 01 enum 1 service_id = SET 12 00 06 long-unsigned 6 class_id 09 06 00 00 0A 00 02 FF octet-string(6) 0-0:10.0.2 logical_name 0F 04 integer 4 index = 4 09 06 76 61 6C 6C 65 79 octet-string(6) "valley" parameter连成一串:01 01 02 02 12 00 01 01 01 02 05 16 01 12 00 06 09 06 00 00 0A 00 02 FF 0F 04 09 06 76 61 6C 6C 65 79
几个编码要点:array的 tag 是0x01、structure是0x02、enum是0x16、integer(有符号)是0x0F、octet-string是0x09、long-unsigned是0x12。parameter的类型是service specific—— 这里因为目标是active_mask(octet-string),所以参数也必须编码成octet-string;用visible-string(tag0x0A)写进去,很多表会直接拒绝。
示例 2:一个脚本做三件事(顺序执行)
每月结算脚本(示例,script_identifier = 0x0002):
actions = [ ① { service_id = 1, class_id = 6, logical_name = 0-0:10.0.2, index = 4, parameter = "peak" } # SET Register activation.active_mask = "peak" → 切回峰费率 ② { service_id = 2, class_id = 7, logical_name = 1-0:99.1.0, index = 2, parameter = integer 0 } # ACTION Profile generic.capture (method 2),data = 0 → 抓一条冻结记录 ③ { service_id = 1, class_id = 6, logical_name = 0-0:10.0.3, index = 4, parameter = "peak_demand" } # SET 第二组寄存器激活掩码 → 需量组也跟着切 ]执行时序(表内):execute(0x0002)→ ① 切回峰费率 → ②capture抓冻结 → ③ 需量组跟着切,全程无返回值、无日志。
关键推论:原文没有规定脚本的事务语义(没有"全部成功或全部回滚"的说法)。工程上必须假定可能部分生效—— 若 ① 成功而 ② 失败(目标对象不存在、buffer 满),表内就停在"费率已切、冻结没抓"的状态,且没有任何属性记录这个失败。所以脚本动作必须按幂等设计(见示例 6)。
示例 3:script_identifier = 0(null script)与全 0 的 dummy action
两种"空"的写法,含义完全不同:
# A. 空脚本:编号 0 是保留值,execute(0) = null script,什么都不做 scripts[0] = { script_identifier = 0x0000, actions = array(0) } 编码: 01 01 02 02 12 00 00 01 00 ↑ script_identifier = 0 ↑ array(0):空的 actions # B. 未配置的动作(NOTE 2 的 dummy):五个元素全 0 scripts[1] = { script_identifier = 0x0001, actions = [ { service_id = 0, class_id = 0, logical_name = 00 00 00 00 00 00, index = 0, parameter = 空 } ] }区别:A适合"这个时段暂时不做事"(Activity calendar/Schedule里把script_selector填0,配置保留但动作空转);B适合"脚本里某一步留作将来扩展"—— 占位但不执行。
注意:原文只说 “all elements 0”,未规定
parameter的零值该编码成什么(null-data / 空octet-string/ 目标类型的零值),实际以厂商实现为准。别把 B 误当成"执行一次无害的写 0 操作"—— 它的语义是"这一步没配"。
示例 4:谁在触发它 —— 四个"调用方"
Script table从不自己醒来,它总是被别人调用。蓝皮书里至少四处引用它,接口形式完全一致(OBIS + 编号的"软引用"):
| 触发方 | class_id | 怎么引用脚本 |
|---|---|---|
Activity calendar | 20 | day_profile_action { start_time, script_logical_name, script_selector } |
Schedule | 10 | schedule_table_entry { …, script_logical_name, script_selector, … } |
Single action schedule | 22 | executed_script { script_logical_name, script_selector } |
Register monitor | 21 | 阈值越限时执行脚本(原文:“a set of scripts … that are executed when the value monitored crosses a threshold”) |
| 外部客户端 | — | 主站直接ACTION execute(data) |
Activity calendar的 Overview 一句话把关系说死了:
蓝皮书原文(Activity calendar, Overview):
“The ‘Activity calendar’ object defines the activation of certain scripts, which can perform different activities inside the logical device. The interface to the IC ‘Script table’ is the same as for the IC ‘Schedule’.”
蓝皮书原文(Activity calendar, day_profile_action):
“script_logical_name: defines the logical_name of the ‘Script table’ object; script_selector: defines the script_identifier of the script to be executed.”
于是完整的费率切换链条是(结合第 6 篇):
Clock (8) ── 提供"现在几点" ↓ Activity calendar (20) ── day_profile 里:start_time = 22:00,script_selector = 1 ↓ (匹配 script_identifier) Script table (9) ── execute(1) → 逐条执行 actions ↓ Register activation (6) ── active_mask 被 SET 成 "valley" → 费率寄存器组切换职责分离得非常干净:Register activation只管"当前选谁",Activity calendar只管"什么时候切",Script table只管"怎么切"。三者可独立配置,这也是 COSEM 对象模型的精髓。
示例 5:主站从外部触发(ACTION execute)
现场调试或补执行时,主站可以直接调execute:
ACTION (class_id = 9, logical_name = 0-0:10.0.0, method_index = 1) data ::= long-unsigned A-XDR(data 部分):12 00 02 # script_identifier = 2(示例 2 的结算脚本) # SN(short name)寻址时,该方法短名为 x + 0x20,参数同样是 long-unsigned外层 ActionRequest APDU 的封装属 xDLMS 部分,此处只展开方法参数。
触发后的验证动作(version = 0 没有executed可查,只能间接验):GET (9, 0-0:10.0.0, 2)读回scripts确认script_identifier存在;GET (6, 0-0:10.0.2, 4)读active_mask确认已改成 “peak”;对 ACTION 型动作则读目标对象的状态属性(如Profile generic的 entries_in_use / 末条时间戳)印证。
示例 6:改脚本引发的"静默失效"
某现场把费率脚本从script_identifier = 1改成2,改完发现 22:00 不再切换,但没有任何告警:
改之前:Activity calendar.day_profile_action{start_time=22:00, script_selector = 1} Script table.scripts = [ {id = 1, actions = [SET active_mask = "valley"]} ] ✔ 命中 改之后:Script table.scripts = [ {id = 2, actions = […]} ] ✘ 1 匹配不上 Activity calendar 仍然持有 script_selector = 1 → 22:00 到了,execute(1),无匹配 → 什么都不执行,不报错根因:调用方存的是script_logical_name+script_selector两个软引用(不是指针),删改脚本不会产生任何一致性检查。
工程对策(三条):
- 改脚本时只覆盖
actions,不要动script_identifier—— 编号一旦投入使用就当主键用。 - 确需增删脚本时,同步检查所有引用方(
Activity calendar的 day_profile、Schedule的 entries、Single action schedule、Register monitor)。 - 脚本动作写成幂等的:用
SET绝对值(“active_mask = valley”),不要用"切换/取反"这类依赖当前状态的动作 —— 因为重复执行(掉电恢复、时间回拨)在Schedule里是被允许的正常行为(见下期)。
5. 工程上容易踩的坑
- 把
execute的data当数组下标:它是long-unsigned的script_identifier,靠匹配定位脚本。匹配不上原文未规定报错行为 —— 表现为"到点了什么都没发生",且无从查证。 script_identifier = 0是保留值:填 0 得到 null script(“no actions to perform”),静默空转。"未配置"要用 0,而不是留空数组或填 9999。index的两套编号混着数:attribute 与 method 各自从1开始,logical_name是 attribute 1、第一个 specific method 是 method 1。写SET时把"业务属性序号"当index用,会写到完全错误的属性上(而SET通常不报错)。- 想在脚本里"读"数据:
service_id只有 write attribute 与 execute specific method 两种,没有 read;且 NOTE 1 限定只能调用不产生响应的方法(capture可以,get_buffer_by_range不行)。 - dummy(全 0)当成"无害空操作":NOTE 2 明确它的语义是"该动作未配置"。反过来,未使用的动作位应留成全 0,别填
class_id = 1/index = 2之类的"占位值"—— 那会真的去写某个对象。 - 软引用脱节:
Activity calendar/Schedule/Single action schedule里存的是 OBIS +script_selector,改脚本不会触发任何一致性校验,失效是静默的。 - 假设脚本是原子的、且能查执行结果:原文未规定事务/回滚语义;
Script table(version = 0)也没有记录执行结果的属性(本卷 raw 未提供executed)。必须按"可能部分生效 + 无据可查"来设计,动作写成幂等的。 parameter类型与权限:parameter是 service specific,必须与目标 attribute / method 的data类型严格一致(active_mask是octet-string,reset的data是integer);另外scripts可写 +execute可外部调用(“or from the outside”),等于给客户端一条远程执行通道,必须置于最高安全等级之下。
6. 小结 & 下期预告
本篇要点:
Script table(class_id = 9, version = 0)是表内可配置的动作清单(宏):Clock决定"何时",它决定"做什么";- 只有 2 个属性 ——
logical_name(static)、scripts(static),scripts是三层结构:array script→script{script_identifier: long-unsigned, actions: array action_specification}; - 一条
action_specification五要素:service_id(1 = write attribute / 2 = execute specific method)、class_id、logical_name(目标对象的 OBIS)、index(attribute / method 各从 1 起)、parameter(service specific); script_identifier = 0保留(null script),全 0 的 action_specification = 未配置(NOTE 2),只能调用无响应的方法(NOTE 1);execute(m,唯一方法)的data是script_identifier,靠匹配执行;可由表内对象或外部客户端触发;同一时刻撞车时较小 index 者优先;- 原文未提供
executed属性、也未规定事务语义—— 按"可能部分生效、无执行记录、需要幂等"来做工程假设。
下一篇(第 10 篇):Schedule(class_id = 10)—— 本篇的搭档。Script table只回答"做什么","在什么时候、按什么周期执行"由Schedule回答:它的entries数组里每个schedule_table_entry都有index、enable、script_logical_name、script_selector、switch_time、validity_window、exec_weekdays(bit-string,周一到周日)、exec_specdays(关联Special days table的 day_id)、begin_date/end_date,还有enable/disable、insert、delete三个方法。我们还会讲它最硬核的两块工程逻辑:掉电恢复后如何补执行丢失的条目,以及时间被向前/向后设置、时间同步、夏令时切换这四种时间变更分别该怎么处理。第 6 篇讲费率切换时提到的"Activity calendar触发Script table",在下下篇会完整串起来。
参考资料:DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》,Script table (class_id = 9, version = 0) 章节,以及 Activity calendar (class_id = 20) 章节中day_profile_action/ Overview 关于脚本引用的描述。文中 2 个属性的名称、static 标记、数据类型、Short name 偏移、execute方法的 m/o 与data类型、以及全部英文引文均与原文一致;示例中的 OBIS(0-0:10.0.0/0-0:10.0.2/1-0:99.1.0)、配置取值与报文字节为帮助理解而构造(实际以设备对象列表与厂商文档为准)。