:Compact data(class_id = 62)—— 把异构属性压成一个 octet-string)
DLMS/COSEM 蓝皮书解读四十七Compact dataclass_id 62—— 把异构属性压成一个 octet-stringxDLMS 里的省字节狂魔系列说明本系列基于 DLMS UA《Blue Book蓝皮书第 16 版 · 第 2 部分》一个接口类一篇。第 46 篇讲的Register tableclass_id 61只能打包同构对象 同一 attribute。本篇的Compact dataclass_id 62走的是另一条路允许异构对象把值压成一个裸的octet-string靠template_idtemplate_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-XDR50 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 1Compact data 0…n class_id 62, version 1属性静态/动态数据类型MinMaxDefShort namelogical_namestaticoctet-string———xcompact_bufferdyn.octet-string———x 0x08capture_objectsstaticarray———x 0x10template_idstaticunsigned———x 0x18template_descriptiondyn.octet-string———x 0x20capture_methodstaticenum———x 0x28方法必选/可选(m/o)Short namereset (data)ox 0x38capture (data)ox 0x401.2 version 0Compact data 0…n class_id 62, version 0属性表与 version 1完全相同含 Short name 偏移x0x08 … x0x28方法名与偏移也相同reset x 0x38capture 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 1capture_object_definition字段class_idlogical_nameattribute_indexdata_index多一个restrictionrestriction_element选择性访问只有相对选择性访问相对当前日期/条目相对 绝对显式日期范围或条目范围data_index语义复杂类型元素选择 / Profile generic buffer 的相对选择性访问同上并额外规定用绝对访问时 MS Byte 高 nibble 必须为 0x0LS Byte 必须为 0x002. 属性逐条解读2.1 logical_name“Identifies the ‘Compact data’ object instance.”原文未对 OBIS 结构作特别规定不像Register table那样有通配符用法。示例中出现过0-0:66.0.0.255这类 OBIS 作为被捕对象class_id 62的template_idattribute_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 1array 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整个 attribute0x0001–0x0FFF复杂 attribute 中的一个元素第一个元素 10x1000–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 v2restriction_element仅 version 1restriction_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:”生成规则三条逐条对应原文第一个字节是0x02structure 的标签紧跟着是元素个数——与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。关键这是服务器自动生成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 与 capture3.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) 的 bufferCompact 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”数据、字节、对比数字均为原文所给我逐字节拆开讲。示例 1Daily billing data原文示例template_id 0原文给出的捕获清单照录Dataclass_idLogical nameattribute_iddata_indexSizeTypeValueTemplate Id620-0:66.0.0.255401unsigned0Unix time10-0:1.1.0.255204double-long-unsigned1374573317Operating status10-0:96.5.0.255201unsigned0x29Error register10-0:97.97.0.255201unsigned0x18Total index37-0:13.83.1.255204double-long-unsigned6422483Index F137-0:13.83.1.255204double-long-unsigned865234Index F237-0:13.83.1.255204double-long-unsigned1234567Index F337-0:13.83.1.255204double-long-unsigned2345678Activity 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原文规则 10A10 个元素capture_objects元素个数11template_idunsignedtag 1706Unix timedouble-long-unsignedtag 61111Operating status、Error registerunsigned06060606Total index、F1、F2、F3double-long-unsigned09Activity calendar nameoctet-string12Event counterlong-unsignedtag 18逐段拆compact_buffer字节值对应000template_id51EE53051374573317Unix time290x29Operating status180x18Error register0061FFD36422483Total index000D33D2865234Index F10012D6871234567Index F20023CACE2345678Index F306 414243444546长度 6 “ABCDEF”Activity calendar name长度在数据区符合 §2.21ED27890Event 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省掉的开销来源之一。示例 2Diagnostic and Alarm data原文示例template_id 1原文清单照录关键项Dataclass_idLogical nameattr_idSizeTypeValueTemplate Id620-0:66.0.0.25541unsigned1Current Diagnostic37-0:96.5.1.25522long-unsigned0x4200Daily Diagnostic37-1:96.5.1.25522long-unsigned0x4108Billing Period Diagnostic17-2:96.5.1.25522long-unsigned0x4308Synchronization event counter10-0:96.15.2.25522long-unsigned763Metrological firmware version17-0:0.2.1.25528octet-string“ABCDEFGH”Metrological event counter10-0:96.15.1.25522long-unsigned1532Non-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 084445464748494A4B02 08→ structure8 个元素11→ unsignedtemplate_id12 12 12 12→ 4 个 long-unsigned09→ octet-string12→ long-unsigned09→ octet-string数据区01template_id1、4200、4108、4308、02FB(763)、08 4142434445464748长度 8 “ABCDEFGH”、05FC(1532)、08 4445464748494A4B长度 8 “DEFGHIJK”。对比原文说同样的数据若从Profile generic的 buffer 读A-XDR 编码是39 bytescompact_buffer是29 bytes。示例 3Logbook reading原文示例template_id 2—— 把整个 Profile generic buffer 压进来这个例子最关键capture_objects只有两个元素——template_id 一个Profile generic(7) 的 buffer0-0:99.98.0.255attribute_index 2。原文假设UNIX 时间戳1374573317、status0x29、为简化起见所有条目值相同、buffer 里共50 条条目。template_id 2 template_description 02 02 11 01 02 02 06 11原文给出的逐项解释照录含义字节含义02 02一个含 2 个元素的 structure11第一个元素是 unsignedtemplate_id01 02 02第二个元素是 array of structure结构内 2 个元素06结构第 1 个元素是 double-long-unsigned11结构第 2 个元素是 unsignedcompact_buffer02 -- template_id 2 32 -- 数组元素个数 500x32 -- 随后 50 × 5 250 字节 合计 252 字节注意3250这个元素个数出现在数据区——完全符合 §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 bytescompact_buffer是252 bytes。省了 200 字节约 44%。条目越多越划算——因为每条目省掉的是重复的02 02structure 头 两个类型 tag。示例 4capture_method选 0 还是 1工程决策场景建议理由主站要某一时刻的快照且希望快照时刻由自己控制(0)显式先capture()再读 buffer时刻明确主站只想读一次拿当前值希望少一次往返(1)隐式读 buffer 即触发捕获一次 GET 搞定多个主站共享同一对象且各自要各自时刻的快照(0)显式隐式模式下任何一次读都会改变 buffer 内容容易互相干扰工程提示非原文隐式模式有副作用——读这个动作会改数据。做审计/取证时要小心读一次就覆盖一次。示例 5capture()不会连带触发被捕对象易错点假设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()。示例 6version 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 的0x1080data_index 0x1080MS 高 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-dataversion 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_objectstemplate_idtemplate_description完整还原原文三个官方示例的实测收益日结算数据50 → 32字节、诊断告警39 → 29字节、事件日志50 条452 → 252字节变长类型的长度在compact_buffer里array的元素个数也在数据区template_id必须是capture_objects的首元素template_description由服务器自动生成改capture_objects会自动清空 buffer 并更新模板capture()不连带触发被捕对象的capture()/reset()reset()只清自己的 bufferversion 1 相比 version 0 增加了restriction字段支持绝对选择性访问日期范围/条目范围且要求data_index的 MS 高 nibble 0x0、LS Byte 0x00。下一篇第 48 篇Status mappingclass_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 与日期为帮助理解而构造非蓝皮书原文。