DLMS/COSEM 蓝皮书解读(四十七):Compact data(class_id = 62)—— 把异构属性压成一个 octet-string:xDLMS 里的"省字节狂魔"
系列说明:本系列基于 DLMS UA《Blue Book(蓝皮书)第 16 版 · 第 2 部分》,一个接口类一篇。第 46 篇讲的
Register table(class_id = 61)只能打包"同构对象 + 同一 attribute"。本篇的Compact data(class_id = 62)走的是另一条路:允许异构对象,把值压成一个裸的octet-string,靠template_id+template_description让主站还原。它有 version 0 与 version 1 两个版本,本文合并讲。
上篇回顾:
Register table(61) 靠"表头 + 单元格"省掉了重复的类型标签,但它要求被捕对象同一个 IC、OBIS 只差 E 组。现实中更多的情况是:“我想一次拿走一个时钟值、一个状态字、三个电量、一个日历名、一个事件计数”——这些对象的class_id各不相同、数据类型五花八门。Compact data就是为这种"杂烩"设计的:它不要求同构,甚至能直接把Profile generic(7) 的 buffer 整块压进来。
0. 为什么需要这个类 —— "标签比数据还大"的问题
先看 A-XDR 的固有开销。用 GET-WITH-LIST 读 10 个值,蓝皮书原文给出的对比是:
| 方式 | 总字节数 | 说明 |
|---|---|---|
| GET-WITH-LIST(普通 A-XDR) | 50 bytes | 每个值都带[0]Data CHOICE 头 + 类型标签 |
Compact data的compact_buffer | 32 bytes | 类型只在template_description里出现一次 |
原文的原话:
蓝皮书原文(
Compact data, Overview):
“The values are stored in the compact_buffer attribute as an octet-string. The set of data types is identified by the template_id attribute. The data type of each attribute captured is held by the template_description attribute. The client can reconstruct the data in the uncompacted form – i.e. including the COSEM attribute descriptor, the data type and the data values – using the capture_objects, template_id and template_description attributes.”
这就是Compact data的全部秘密:把"类型信息"从数据里剥离出来,另存一份模板(template_description),数据区(compact_buffer)只留裸值。主站拿到两样东西后能完整还原成"带 COSEM attribute 描述符 + 数据类型 + 值"的标准形式。
对窄带信道(PLC、无线、LoRa)来说,50 → 32 字节意味着省掉 36% 的空口开销,一个月抄几次、几万台表,累计相当可观。这是它存在的根本理由。
1. 类蓝图
1.1 version = 1
Compact data 0…n class_id = 62, version = 1| 属性 | 静态/动态 | 数据类型 | Min | Max | Def | Short name |
|---|---|---|---|---|---|---|
| logical_name | static | octet-string | — | — | — | x |
| compact_buffer | dyn. | octet-string | — | — | — | x + 0x08 |
| capture_objects | static | array | — | — | — | x + 0x10 |
| template_id | static | unsigned | — | — | — | x + 0x18 |
| template_description | dyn. | octet-string | — | — | — | x + 0x20 |
| capture_method | static | enum | — | — | — | x + 0x28 |
| 方法 | 必选/可选(m/o) | Short name |
|---|---|---|
| reset (data) | o | x + 0x38 |
| capture (data) | o | x + 0x40 |
1.2 version = 0
Compact data 0…n class_id = 62, version = 0属性表与 version 1完全相同(含 Short name 偏移x+0x08 … x+0x28),方法名与偏移也相同(reset= x + 0x38,capture= x + 0x40)。
1.3 两个版本的真正差异
蓝皮书原文(
Compact dataversion 1, Overview):
“NOTE This version 1 supports both relative and absolute selective access.”
差异集中在capture_objects里的capture_object_definition结构:
| 项目 | version 0 | version 1 |
|---|---|---|
capture_object_definition字段 | class_id+logical_name+attribute_index+data_index | 多一个restriction(restriction_element) |
| 选择性访问 | 只有相对选择性访问(相对当前日期/条目) | 相对 + 绝对(显式日期范围或条目范围) |
data_index语义 | 复杂类型元素选择 / Profile generic buffer 的相对选择性访问 | 同上,并额外规定:用绝对访问时 MS Byte 高 nibble 必须为 0x0,LS Byte 必须为 0x00 |
2. 属性逐条解读
2.1 logical_name
“Identifies the ‘Compact data’ object instance.”
原文未对 OBIS 结构作特别规定(不像Register table那样有通配符用法)。示例中出现过0-0:66.0.0.255这类 OBIS 作为被捕对象(class_id = 62的template_id,attribute_index = 4)。
2.2 compact_buffer —— 核心数据区
蓝皮书原文(
Compact data, compact_buffer):
“Contains the values of the attributes captured as an octet-string. When the data captured is of type octet-string, bit-string, visible-string, utf8-string or array the length is also included here.”
注意最后一句:变长类型(octet-string、bit-string、visible-string、utf8-string)和array的长度是放在compact_buffer里的,而不是放在template_description里——因为长度每次捕获可能变,类型模板是固定的。
2.3 capture_objects —— 捕获清单
“Specifies the list of COSEM object attributes that are assigned to the ‘Compact data’ object instance. When defining the capture_objects attribute, circular references shall be avoided. The template_id attribute shall be the first element in the capture_objects array.”
两条硬规则:
- 必须避免循环引用(
circular references shall be avoided)——别让 A 捕 B、B 又捕 A。 template_id必须是capture_objects数组的第一个元素。
结构定义(version 1):
array capture_object_definition capture_object_definition ::= structure { class_id: long-unsigned, logical_name: octet-string, attribute_index: integer, data_index: long-unsigned, restriction: restriction_element -- 仅 version 1 }attribute_index:“attribute_index 1 refers to the 1st attribute (i.e. the logical_name), attribute_index 2 to the 2nd attribute etc.; attribute_index 0 refers to all public attributes”;data_index:“is a pointer selecting one or several specific elements of an attribute with a complex data type (structure or array)”:- attribute 是简单类型 →
data_index无意义; - attribute 是 structure/array(除 Profile generic 的 buffer 外)→
data_index指向其中某个元素; - attribute 是
Profile generic的 buffer →data_index承载相对当前日期/条目的选择性访问参数。
- attribute 是简单类型 →
data_index 的编码(两版本共用)
data_index: MS-Byte LS-Byte (Upper nibble) (Lower nibble)| 取值 | 含义 |
|---|---|
0x0000 | 整个 attribute |
0x0001–0x0FFF | 复杂 attribute 中的一个元素(第一个元素 = 1) |
0x1000–0xFFFF | 对Profile genericbuffer 的选择性访问(相对方式) |
MS-Byte 高 nibble(选时间段/条目)(原文完整表格):
| 值 | 含义 |
|---|---|
0xF | 最近若干个完整月 + 本月零点第一条 |
0xE | 最近若干个完整日 + 今日零点第一条 |
0xD | 最近若干个完整小时 + 当前小时第一条 |
0xC | 最近若干个完整分钟 + 当前分钟第一条 |
0xB | 最近若干秒 |
0xA | 最近若干个完整月含当前月(截至现在全部) |
0x9 | 最近若干个完整日含当日 |
0x8 | 最近若干个完整小时含当前小时 |
0x7 | 最近若干个完整分钟含当前分钟 |
0x6–0x2 | 仅适用于PushIC version 2:上次确认之后的月/日/小时/分钟/条目数 |
0x1 | 最近若干条条目 |
0x0 | 整个 attribute,或复杂类型中的单个元素 |
MS-Byte 低 nibble(选列):0x0= 所有列;0x1–0xF= 从第 1 列开始的 N 列。
LS-Byte:选时间段时 = 时间段个数(0x00= 全部时间段);选条目时 = 条目个数(0x00= 全部条目)。
原文给的 5 个例子(照录):
| 例子 | 值 | 含义 |
|---|---|---|
| Example 1 | 0xE401 | 最近一个完整日的条目,取前 4 列 |
| Example 2 | 0xA300 | 0 个完整月 + 当前月,取前 3 列 |
| Example 3 | 0x800C | 最近完整 12 小时,全部列 |
| Example 4 | 0x1080 | 最近 128 条条目,全部列 |
| Example 5 | 0x4608 | 上次确认之后最近完整 8 小时,取前 6 列(Push v2) |
restriction_element(仅 version 1)
restriction_element ::= structure { restriction_type: enum: (0) none, (1) restriction by date, (2) restriction by entry restriction_value: CHOICE { null-data, restriction_by_date, restriction_by_entry } } restriction_by_date ::= structure { from_date: octet-string, to_date: octet-string } restriction_by_entry ::= structure { from_entry: double-long-unsigned, to_entry: double-long-unsigned }使用绝对选择性访问时,data_index必须:MS Byte 高 nibble = 0x0、MS Byte 低 nibble = 0x0–0xF、LS Byte = 0x00。
“otherwise, the restriction_type element holds (0) none and the restriction_value element holds null-data. This choice shall be taken also if relative selective access is to be used”—— 用相对方式时,也要填
(0) none + null-data,不能省略该字段。
2.4 template_id —— 模板标识
“Contains the identifier of the template. It shall uniquely identify the instance of the ‘Compact data’ IC and the template_description.”
它必须是capture_objects的第一个元素,示例里被捕对象写的是class_id = 62、logical_name = 0-0:66.0.0.255、attribute_index = 4、data_index = 0——即引用自身对象的template_id属性。
2.5 template_description —— 类型模板(自动生成)
蓝皮书原文(
Compact data, template_description):
“Provides the data type of each attribute captured. It is an octet-string generated automatically by the server upon the programming of the capture_objects and it has the following structure:”
生成规则(三条,逐条对应原文):
- 第一个字节是
0x02(structure 的标签); - 紧跟着是元素个数——与
capture_objects数组元素个数相同——按变长整数编码; - 再后面按
capture_objects顺序,逐个给出数据类型:- 简单类型 →一个字节,即该类型的 tag。其中
bit-string [4]、octet-string [9]、visible-string [10]、utf8-string [12]的长度属于compact_buffer里的数据; array [1]→ 单字节0x01,后面跟元素类型描述;元素个数在compact_buffer里;structure [2]→ 单字节0x02,后面跟结构内元素个数,再跟每个元素的 tag。
- 简单类型 →一个字节,即该类型的 tag。其中
关键:这是服务器自动生成(generated automatically by the server),主站不该去写它。
2.6 capture_method —— 显式还是隐式
蓝皮书原文(
Compact data, capture_method):
“Defines the way the compact_buffer is updated.”
enum (0) Capture upon invoking the capture (data) method. This may occur remotely or locally (explicit capturing), (1) Capture upon reading the compact_buffer attribute (implicit capturing).- (0) 显式:主站(或本地脚本)调
capture()才更新; - (1) 隐式:读
compact_buffer这个动作本身触发捕获——主站一次 GET 就拿到"刚刚捕获的当前值",省一次方法调用往返。
3. 方法:reset 与 capture
3.1 reset (data)
“Clears the compact_buffer. After invoking this method the buffer holds an octet-string of 0 length until a new capture takes place. This call does not trigger any additional operations of the capture objects. Specifically, it does not reset any captured attributes.”
data ::= integer(0)
reset只清空自己的 buffer,不会连带触发被捕对象的任何动作(特别是不会 reset 被捕的 attribute)。
3.2 capture (data)
“Copies the values of the attributes into the compact_buffer by reading each capture object. This call does not trigger any additional operations within the capture objects such as capture () or reset ().”
data ::= integer(0)
非常重要的一条:调用capture()不会连带触发被捕对象自己的capture()或reset()。也就是说,如果capture_objects里有Profile generic(7) 的 buffer,Compact data的capture()只是"读走当前 buffer 内容",不会让那个Profile generic去捕获一条新记录。
3.3 改 capture_objects 的连带效应
“Any modification of the capture_objects shall reset the compact_buffer and automatically update the template_description.”
改清单 → 自动清空 buffer + 自动重算template_description。(对比Register table(61):那里是自动 reset 并传播到 profile;这里是自动 reset + 更新模板,且原文没说传播。)
4. 【实战举例】
以下三个示例直接取自蓝皮书原文的 “Examples for using compact data”(数据、字节、对比数字均为原文所给),我逐字节拆开讲。
示例 1:Daily billing data(原文示例,template_id = 0)
原文给出的捕获清单(照录):
| Data | class_id | Logical name | attribute_id | data_index | Size | Type | Value |
|---|---|---|---|---|---|---|---|
| Template Id | 62 | 0-0:66.0.0.255 | 4 | 0 | 1 | unsigned | 0 |
| Unix time | 1 | 0-0:1.1.0.255 | 2 | 0 | 4 | double-long-unsigned | 1374573317 |
| Operating status | 1 | 0-0:96.5.0.255 | 2 | 0 | 1 | unsigned | 0x29 |
| Error register | 1 | 0-0:97.97.0.255 | 2 | 0 | 1 | unsigned | 0x18 |
| Total index | 3 | 7-0:13.83.1.255 | 2 | 0 | 4 | double-long-unsigned | 6422483 |
| Index F1 | 3 | 7-0:13.83.1.255 | 2 | 0 | 4 | double-long-unsigned | 865234 |
| Index F2 | 3 | 7-0:13.83.1.255 | 2 | 0 | 4 | double-long-unsigned | 1234567 |
| Index F3 | 3 | 7-0:13.83.1.255 | 2 | 0 | 4 | double-long-unsigned | 2345678 |
| Activity calendar name | 20 | 0-0:13.0.0.255 | 2 | 0 | 6 | octet-string | “ABCDEF” |
| Event counter | 1 | 0-0:96.15.1.255 | 2 | 0 | 2 | long-unsigned | 7890 |
原文给出的对象属性:
template_id (unsigned) = 0 template_description (octet-string) = 02 0A 11 06 11 11 06 06 06 06 09 12 compact_buffer (octet-string, 32 bytes) = 00 51EE5305 29 18 0061FFD3 000D33D2 0012D687 0023CACE 06414243444546 1ED2逐字节拆template_description:
| 字节 | 含义 |
|---|---|
02 | structure 的 tag(原文规则 1) |
0A | 10 个元素(=capture_objects元素个数) |
11 | template_id:unsigned(tag 17) |
06 | Unix time:double-long-unsigned(tag 6) |
1111 | Operating status、Error register:unsigned |
06060606 | Total index、F1、F2、F3:double-long-unsigned |
09 | Activity calendar name:octet-string |
12 | Event counter:long-unsigned(tag 18) |
逐段拆compact_buffer:
| 字节 | 值 | 对应 |
|---|---|---|
00 | 0 | template_id |
51EE5305 | 1374573317 | Unix time |
29 | 0x29 | Operating status |
18 | 0x18 | Error register |
0061FFD3 | 6422483 | Total index |
000D33D2 | 865234 | Index F1 |
0012D687 | 1234567 | Index F2 |
0023CACE | 2345678 | Index F3 |
06 414243444546 | 长度 6 + “ABCDEF” | Activity calendar name(长度在数据区,符合 §2.2) |
1ED2 | 7890 | Event counter |
省了多少:原文给出 GET-WITH-LIST 的 A-XDR 编码合计50 bytes,而compact_buffer只有32 bytes。
原文对 GET-WITH-LIST 那段编码的注释:“The leading 00-s in each element are there to indicate the CHOICE ‘Data’ in Get-Data-Result.”—— 每个元素前面那个
00就是被Compact data省掉的开销来源之一。
示例 2:Diagnostic and Alarm data(原文示例,template_id = 1)
原文清单(照录关键项):
| Data | class_id | Logical name | attr_id | Size | Type | Value |
|---|---|---|---|---|---|---|
| Template Id | 62 | 0-0:66.0.0.255 | 4 | 1 | unsigned | 1 |
| Current Diagnostic | 3 | 7-0:96.5.1.255 | 2 | 2 | long-unsigned | 0x4200 |
| Daily Diagnostic | 3 | 7-1:96.5.1.255 | 2 | 2 | long-unsigned | 0x4108 |
| Billing Period Diagnostic | 1 | 7-2:96.5.1.255 | 2 | 2 | long-unsigned | 0x4308 |
| Synchronization event counter | 1 | 0-0:96.15.2.255 | 2 | 2 | long-unsigned | 763 |
| Metrological firmware version | 1 | 7-0:0.2.1.255 | 2 | 8 | octet-string | “ABCDEFGH” |
| Metrological event counter | 1 | 0-0:96.15.1.255 | 2 | 2 | long-unsigned | 1532 |
| Non-metrological firmware version | 1 | 7-1:0.2.1.255 | 2 | 8 | octet-string | “DEFGHIJK” |
template_id = 1 template_description = 02 08 11 12 12 12 12 09 12 09 compact_buffer (29 bytes) = 01 4200 4108 4308 02FB 084142434445464748 05FC 084445464748494A4B02 08→ structure,8 个元素;11→ unsigned(template_id);12 12 12 12→ 4 个 long-unsigned;09→ octet-string;12→ long-unsigned;09→ octet-string;- 数据区:
01(template_id=1)、4200、4108、4308、02FB(=763)、08 4142434445464748(长度 8 + “ABCDEFGH”)、05FC(=1532)、08 4445464748494A4B(长度 8 + “DEFGHIJK”)。
对比:原文说同样的数据若从Profile generic的 buffer 读,A-XDR 编码是39 bytes;compact_buffer是29 bytes。
示例 3:Logbook reading(原文示例,template_id = 2)—— 把整个 Profile generic buffer 压进来
这个例子最关键:capture_objects只有两个元素——template_id+ 一个Profile generic(7) 的 buffer(0-0:99.98.0.255,attribute_index = 2)。
原文假设:UNIX 时间戳1374573317、status0x29、为简化起见所有条目值相同、buffer 里共50 条条目。
template_id = 2 template_description = 02 02 11 01 02 02 06 11原文给出的逐项解释(照录含义):
| 字节 | 含义 |
|---|---|
02 02 | 一个含 2 个元素的 structure |
11 | 第一个元素是 unsigned(template_id) |
01 02 02 | 第二个元素是 array of structure,结构内 2 个元素 |
06 | 结构第 1 个元素是 double-long-unsigned |
11 | 结构第 2 个元素是 unsigned |
compact_buffer:
02 -- template_id = 2 32 -- 数组元素个数 = 50(0x32) -- 随后 50 × 5 = 250 字节 合计 252 字节注意32(50)这个"元素个数"出现在数据区——完全符合 §2.5 的规则:“the number of elements in the array is part of the data held in the compact_buffer”。
对比:原文说从Profile generic的 buffer 读同样数据,A-XDR 编码是452 bytes;compact_buffer是252 bytes。省了 200 字节,约 44%。条目越多越划算——因为每条目省掉的是重复的02 02(structure 头)+ 两个类型 tag。
示例 4:capture_method选 0 还是 1(工程决策)
| 场景 | 建议 | 理由 |
|---|---|---|
| 主站要"某一时刻的快照",且希望快照时刻由自己控制 | (0)显式 | 先capture(),再读 buffer,时刻明确 |
| 主站只想"读一次拿当前值",希望少一次往返 | (1)隐式 | 读 buffer 即触发捕获,一次 GET 搞定 |
| 多个主站共享同一对象,且各自要各自时刻的快照 | (0)显式 | 隐式模式下任何一次读都会改变 buffer 内容,容易互相干扰 |
工程提示(非原文):隐式模式有副作用——读这个动作会改数据。做审计/取证时要小心,读一次就覆盖一次。
示例 5:capture()不会连带触发被捕对象(易错点)
假设capture_objects里有一个Profile generic(7) 的 buffer。主站调Compact data.capture():
- ✅ 把该
Profile generic当前 buffer 的内容拷进compact_buffer; - ❌不会让那个
Profile generic捕获一条新记录(原文:“does not trigger any additional operations within the capture objects such as capture () or reset ()”)。
如果想让Profile generic先捕获再压缩,顺序必须是:先调Profile generic.capture(),再调Compact data.capture()。
示例 6:version 1 的绝对选择性访问(构造示例,非原文)
需求:只读某Profile generic在2024-01-01 00:00:00到2024-01-31 23:59:59之间的条目,且只要前 3 列。
capture_object_definition = { class_id = 7, logical_name = 1-0:99.1.0.255, attribute_index = 2, data_index = 0x0300, -- MS 高 nibble = 0x0(绝对访问),低 nibble = 0x3(前 3 列),LS = 0x00 restriction = { restriction_type = 1, -- restriction by date restriction_value = { from_date = <date-time octet-string>, to_date = <date-time octet-string> } } }说明:此示例为帮助理解构造(非蓝皮书原文),OBIS 与日期取值以项目规范为准。约束来自原文:绝对访问时
data_index的 MS 高 nibble 必须为0x0、LS Byte 必须为0x00。
示例 7:相对选择性访问(用原文 Example 4 的0x1080)
data_index = 0x1080:
- MS 高 nibble =
0x1→“Last number of entries”(最近若干条条目); - MS 低 nibble =
0x0→ 所有列; - LS Byte =
0x80= 128 → 最近 128 条。
原文注释:“0x1080: The last 128 entries are selected. All columns are included.”
5. 工程上容易踩的坑
- 以为
capture()会连带触发被捕对象的capture():不会。原文明确“does not trigger any additional operations within the capture objects such as capture () or reset ()”。顺序要自己排。 - 忘了
template_id必须是capture_objects第一个元素:原文用了shall。 - 主站去写
template_description:它是服务器自动生成的(“generated automatically by the server upon the programming of the capture_objects”)。 - 变长类型的长度找错地方:
octet-string/bit-string/visible-string/utf8-string/array的长度在数据区(compact_buffer),类型模板里没有。 - 循环引用:原文“circular references shall be avoided”。
- 改
capture_objects后 buffer 被清空:“Any modification of the capture_objects shall reset the compact_buffer”——改清单要重捕获。 - version 0 / version 1 混淆:version 0 的
capture_object_definition没有restriction字段,只有相对选择性访问;拿 version 1 的结构去配 version 0 设备会失败。 - 用绝对访问时
data_index没按约束填:MS 高 nibble 必须0x0、LS Byte 必须0x00。 - 用相对访问时省略了
restriction:原文规定此时也要填(0) none + null-data(version 1 有该字段)。 - 隐式捕获(
capture_method = 1)的副作用:读一次改一次,多主站共享或审计场景要慎用。 reset的作用范围:只清自己的 buffer,不 reset 被捕的 attribute。- 把
0x6–0x2用错对象:原文说这些值“apply only for the Push interface class version 2”。
6. 小结 & 下期预告
本篇要点:
Compact data(62) 把异构对象属性压成裸octet-string,类型信息另存template_description,主站靠capture_objects+template_id+template_description完整还原;- 原文三个官方示例的实测收益:日结算数据50 → 32字节、诊断告警39 → 29字节、事件日志(50 条)452 → 252字节;
- 变长类型的长度在
compact_buffer里,array的元素个数也在数据区; template_id必须是capture_objects的首元素;template_description由服务器自动生成;改capture_objects会自动清空 buffer 并更新模板;capture()不连带触发被捕对象的capture()/reset();reset()只清自己的 buffer;- version 1 相比 version 0 增加了
restriction字段,支持绝对选择性访问(日期范围/条目范围),且要求data_index的 MS 高 nibble =0x0、LS Byte =0x00。
下一篇(第 48 篇):Status mapping(class_id = 63)—— 只有三个 attribute,却解决了一个很实际的问题:不同厂家状态字的位序各不相同,如何把"第 N 位"映射到标准参考表里的条目。
参考资料:DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》,Compact data(class_id = 62, version = 0 与 version = 1) 章节,含 “Examples for using compact data”(Daily billing data / Diagnostic and Alarm data / Logbook reading);文中属性名、数据类型、Short name 偏移、方法 m/o、data_index 编码表、template_description生成规则与三组示例字节均与原文一致;示例 6 的 OBIS 与日期为帮助理解而构造(非蓝皮书原文)。