☰
DLMS/COSEM 蓝皮书解读(四十七):Compact data(class_id = 62)—— 把异构属性压成一个 octet-string
2026/10/24 5:32:24 网站建设 项目流程

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_buffer32 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
属性静态/动态数据类型MinMaxDefShort name
logical_namestaticoctet-string———x
compact_bufferdyn.octet-string———x + 0x08
capture_objectsstaticarray———x + 0x10
template_idstaticunsigned———x + 0x18
template_descriptiondyn.octet-string———x + 0x20
capture_methodstaticenum———x + 0x28
方法必选/可选(m/o)Short name
reset (data)ox + 0x38
capture (data)ox + 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 0version 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.”

两条硬规则:

  1. 必须避免循环引用(circular references shall be avoided)——别让 A 捕 B、B 又捕 A。
  2. 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承载相对当前日期/条目的选择性访问参数。
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 10xE401最近一个完整日的条目,取前 4 列
Example 20xA3000 个完整月 + 当前月,取前 3 列
Example 30x800C最近完整 12 小时,全部列
Example 40x1080最近 128 条条目,全部列
Example 50x4608上次确认之后最近完整 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:”

生成规则(三条,逐条对应原文):

  1. 第一个字节是0x02(structure 的标签);
  2. 紧跟着是元素个数——与capture_objects数组元素个数相同——按变长整数编码;
  3. 再后面按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。

关键:这是服务器自动生成(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)

原文给出的捕获清单(照录):

Dataclass_idLogical nameattribute_iddata_indexSizeTypeValue
Template Id620-0:66.0.0.255401unsigned0
Unix time10-0:1.1.0.255204double-long-unsigned1374573317
Operating status10-0:96.5.0.255201unsigned0x29
Error register10-0:97.97.0.255201unsigned0x18
Total index37-0:13.83.1.255204double-long-unsigned6422483
Index F137-0:13.83.1.255204double-long-unsigned865234
Index F237-0:13.83.1.255204double-long-unsigned1234567
Index F337-0:13.83.1.255204double-long-unsigned2345678
Activity calendar name200-0:13.0.0.255206octet-string“ABCDEF”
Event counter10-0:96.15.1.255202long-unsigned7890

原文给出的对象属性:

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:

字节含义
02structure 的 tag(原文规则 1)
0A10 个元素(=capture_objects元素个数)
11template_id:unsigned(tag 17)
06Unix time:double-long-unsigned(tag 6)
1111Operating status、Error register:unsigned
06060606Total index、F1、F2、F3:double-long-unsigned
09Activity calendar name:octet-string
12Event counter:long-unsigned(tag 18)

逐段拆compact_buffer:

字节值对应
000template_id
51EE53051374573317Unix time
290x29Operating status
180x18Error register
0061FFD36422483Total index
000D33D2865234Index F1
0012D6871234567Index F2
0023CACE2345678Index F3
06 414243444546长度 6 + “ABCDEF”Activity calendar name(长度在数据区,符合 §2.2)
1ED27890Event 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)

原文清单(照录关键项):

Dataclass_idLogical nameattr_idSizeTypeValue
Template Id620-0:66.0.0.25541unsigned1
Current Diagnostic37-0:96.5.1.25522long-unsigned0x4200
Daily Diagnostic37-1:96.5.1.25522long-unsigned0x4108
Billing Period Diagnostic17-2:96.5.1.25522long-unsigned0x4308
Synchronization event counter10-0:96.15.2.25522long-unsigned763
Metrological firmware version17-0:0.2.1.25528octet-string“ABCDEFGH”
Metrological event counter10-0:96.15.1.25522long-unsigned1532
Non-metrological firmware version17-1:0.2.1.25528octet-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 084445464748494A4B
  • 02 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. 工程上容易踩的坑

  1. 以为capture()会连带触发被捕对象的capture():不会。原文明确“does not trigger any additional operations within the capture objects such as capture () or reset ()”。顺序要自己排。
  2. 忘了template_id必须是capture_objects第一个元素:原文用了shall。
  3. 主站去写template_description:它是服务器自动生成的(“generated automatically by the server upon the programming of the capture_objects”)。
  4. 变长类型的长度找错地方:octet-string/bit-string/visible-string/utf8-string/array的长度在数据区(compact_buffer),类型模板里没有。
  5. 循环引用:原文“circular references shall be avoided”。
  6. 改capture_objects后 buffer 被清空:“Any modification of the capture_objects shall reset the compact_buffer”——改清单要重捕获。
  7. version 0 / version 1 混淆:version 0 的capture_object_definition没有restriction字段,只有相对选择性访问;拿 version 1 的结构去配 version 0 设备会失败。
  8. 用绝对访问时data_index没按约束填:MS 高 nibble 必须0x0、LS Byte 必须0x00。
  9. 用相对访问时省略了restriction:原文规定此时也要填(0) none + null-data(version 1 有该字段)。
  10. 隐式捕获(capture_method = 1)的副作用:读一次改一次,多主站共享或审计场景要慎用。
  11. reset的作用范围:只清自己的 buffer,不 reset 被捕的 attribute。
  12. 把0x6–0x2用错对象:原文说这些值“apply only for the Push interface class version 2”。

6. 小结 & 下期预告

本篇要点:

  1. Compact data(62) 把异构对象属性压成裸octet-string,类型信息另存template_description,主站靠capture_objects+template_id+template_description完整还原;
  2. 原文三个官方示例的实测收益:日结算数据50 → 32字节、诊断告警39 → 29字节、事件日志(50 条)452 → 252字节;
  3. 变长类型的长度在compact_buffer里,array的元素个数也在数据区;
  4. template_id必须是capture_objects的首元素;template_description由服务器自动生成;改capture_objects会自动清空 buffer 并更新模板;
  5. capture()不连带触发被捕对象的capture()/reset();reset()只清自己的 buffer;
  6. 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 与日期为帮助理解而构造(非蓝皮书原文)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询