DLMS/COSEM 蓝皮书解读(二十三):Utility tables(class_id = 26)—— 把"任意表数据"封装成 COSEM 对象的通用集装箱
系列说明:本系列基于 DLMS UA《Blue Book(蓝皮书)第 16 版 · 第 2 部分》,一个接口类一篇。第 21、22 篇都在讲"通信端口怎么配"(载波双绞线、有线 M-Bus)。本篇话题一转:
Utility tables(class_id = 26)是一个与通信介质无关、纯数据载体的类——它把任意"表(table)"数据(ANSI 标准表或厂商自定义表)封装成一个 COSEM 对象,用table_ID标识、用buffer存内容,并支持按偏移/按索引的选择性访问。
上篇回顾:第 22 篇把 M-Bus 从站端口配好了,设备能被主站找到。但很多时候主站想搬的不只是 DLMS 原生的属性数据,还有别的体系定义的"表"(比如 ANSI C12 的标准表、或表厂私有的配置表)。这些结构 DLMS 并没有对应接口类。本篇
Utility tables就是干这个的——一个"通用集装箱",让非 DLMS 原生的表数据也能在 COSEM 对象模型里被寻址、读取。
0. 为什么需要这个类
DLMS/COSEM 的强项是"用接口类描述对象"。但现实世界里,表计里还躺着大量不是 DLMS 原生定义的数据结构:
- ANSI 标准表(ANSI C12 系列定义的那些表,比如设备配置表、历史表);
- 厂商自定义表(表厂私有的参数表、诊断表);
这些数据如果硬要映射成Data/Register之类的接口类,要么丢结构、要么极繁琐。蓝皮书给的解法是——直接整张表塞进一个对象:
蓝皮书原文(
Utility tables, Overview):
“This IC allows encapsulating table data. Each ‘table’ is represented by an instance of this IC, identified by its logical name.”
一句话:一个表 = 一个Utility tables实例,用logical_name寻址、table_ID标明是哪张表、buffer装原始字节。它就像一个"万能集装箱",把任何表结构原样搬进 COSEM 对象模型,主站再按自己的理解去解析buffer里的字节。
1. 类蓝图
蓝皮书原文(
Utility tables):
“Utility tables 0…n class_id = 26, version = 0”
Utility tables 0...n class_id = 26, version = 0| 属性 | 静态/动态 | 数据类型 | Min | Max | Def | Short name |
|---|---|---|---|---|---|---|
| logical_name | static | octet-string | x | |||
| table_ID | static | long-unsigned | x + 0x08 | |||
| length | static | double-long-unsigned | x + 0x10 | |||
| buffer | static | octet-string | x + 0x18 |
没有方法(Specific methods 一栏为空)。全部static,靠 GET/SET 访问。
2. 属性逐条解读
2.1 logical_name
标识本Utility tables对象实例。6 字节 OBIS,static,基址x。
蓝皮书原文(logical_name):“Identifies the ‘Utility tables’ object instance.”
2.2 table_ID
表号(Table number)。
蓝皮书原文(table_ID):
“Table number. This table number is as specified in the ANSI standard and may be either a standard table or a manufacturer’s table.”
static,long-unsigned,Short namex + 0x08。两种来源:
- ANSI 标准表(standard table,编号由 ANSI 标准规定);
- 厂商自定义表(manufacturer’s table,编号由厂商规定)。
背景说明(非蓝皮书原文,属 ANSI C12 领域常识):ANSI C12.19 把表分成标准表(如 0 号表是设备标识、1 号表是设备配置)和厂商表(通常编号高位段,如 0x8000 起)。
table_ID具体取值含义取决于你的设备遵循哪份表定义,蓝皮书只说"按 ANSI 标准"。
2.3 length
buffer中的字节数。
蓝皮书原文(length):“Number of octets in table buffer.”
static,double-long-unsigned(4 字节无符号,最大约 4 GB),Short namex + 0x10。注意它是"字节计数",不是"元素个数"——buffer是裸 octet-string,内部怎么分元素由表定义决定。
2.4 buffer
表的实际内容(原始字节)。
蓝皮书原文(buffer):“Contents of the table.”
static,octet-string,Short namex + 0x18。这就是那个"集装箱"本身——表里每一字节原样存放,DLMS 不解释其内部结构。
3. 没有方法:靠选择性访问(selective access)取数
本类没有 ACTION 方法。它真正的"巧思"在buffer 属性的选择性访问上——因为buffer可能很大、内部结构复杂,主站往往只想取其中一段,而不是整张表。
蓝皮书原文(
Utility tables, Selective access):
“Selective access (see ) to the attribute buffer may be available (optional). The selective access parameters are defined in .”
注意原文写的是may be available (optional)——也就是说选择性访问是可选的,不是每个实现都支持。不支持时主站只能整表 GET。
支持时提供两种 access selector:
| Access selector | 参数 | 含义 |
|---|---|---|
| 1 | offset_access | 按偏移 + 字节数访问(offset_selector) |
| 2 | index_access | 按元素层级索引 + 元素数访问(index_selector) |
3.1 offset_selector(selector = 1,按字节偏移)
offset_selector ::= structure { Offset: double-long-unsigned // 相对表起始的字节偏移 Count: long-unsigned // 请求/传输的字节数 }3.2 index_selector(selector = 2,按层级索引)
index_selector ::= structure { Index: array long-unsigned // 标识表层级中元素的索引序列 Count: long-unsigned // 请求/传输的元素数; // 请求中 Count=0 表示"从选择点起整棵子树" }蓝皮书原文(index_selector):
“Values of count greater than 1 return up to that many elements. A value of zero, when given in the context of a request, refers to the entire sub-tree of the hierarchy starting at the selection point.”
关键语义:index_access的Index是一个数组(long-unsigned 序列),表达的是"层级路径"——比如[1, 2]表示"第 1 层的第 2 个子元素";Count表示要取几个元素,Count = 0在请求里表示"从选择点开始整棵子树全要"。
4. 【实战举例】
示例 1:一张厂商配置表的标准对象建模
(以下 OBIS / table_ID / 字节为示例,非蓝皮书原文,实际以设备对象列表为准)
| 属性 | 取值 | 说明 |
|---|---|---|
| logical_name | 0-0:65.16.1.255 | 示例 OBIS |
| table_ID | 0x0001 | 厂商自定义表 0x0001(示例编号) |
| length | 256 | buffer 共 256 字节 |
| buffer | 256 字节原始数据 | 厂商表内部字节 |
主站想读整张表,就 GETbuffer(短名x + 0x18)。但 256 字节一次拉可能过大,于是用选择性访问。
示例 2:按偏移取前 16 字节(selector = 1,offset_access)
请求 GETbuffer并带access_selector = 1,参数是offset_selector{ Offset = 0, Count = 16 }:
A-XDR 编码(structure标签02;Offset为double-long-unsigned标签06、4 字节 =00 00 00 00;Count为long-unsigned标签12、2 字节 =00 10= 16):
02 0A 06 04 00 00 00 00 12 02 00 10 (示例:selector=1 参数,offset=0, count=16)含义:从表头偏移 0 起,取 16 字节。主站拿到这 16 字节后按厂商表定义解析。
示例 3:按层级索引取元素(selector = 2,index_access)
请求 GETbuffer带access_selector = 2,参数index_selector{ Index = [1, 2], Count = 1 }:
A-XDR 编码(structure标签02;Index为array标签01、元素数 2、元素均为long-unsigned标签12:值 1=00 01、值 2=00 02;Count为long-unsigned=00 01):
02 0E 01 02 12 02 00 01 12 02 00 02 12 02 00 01 (示例:selector=2 参数,Index=[1,2], Count=1)含义:取"第 1 层第 2 个子元素"那 1 个元素。Count = 0时则取从该选择点起的整棵子树。
示例 4:SN 短名访问
base name 为x(由 logical_name 派生的 13 位短名):
| 属性 | Short name |
|---|---|
| table_ID | x + 0x08 |
| length | x + 0x10 |
| buffer | x + 0x18 |
例如想快速确认缓冲区大小,GET 短名x + 0x10(length,double-long-unsigned)即可,不必拉整个buffer。
示例 5:与Profile generic(class_id = 7)的区别
两者都装"结构化数据",但定位完全不同:
| 维度 | Utility tables (26) | Profile generic (7) |
|---|---|---|
| 数据来源 | 任意 ANSI/厂商表(外部定义结构) | COSEM 捕获对象(capture_objects 定义) |
| 内部结构 | DLMS 不解释,纯字节 | 由 capture_objects 明确定义列 |
| 选择性访问 | offset / index(可选) | range_descriptor / entry_descriptor |
| 用途 | 搬运非 DLMS 原生表 | 计量档案/负荷曲线 |
一句话:Profile generic管"DLMS 自己采的曲线",Utility tables管"别人定义的表"。
示例 6:用 offset_access 读一张厂商表并逐字节解析
假设table_ID = 0x0001的厂商表,前 16 字节布局(以下布局为示例,非蓝皮书原文):
| 字节偏移 | 长度 | 字段 | 示例值 |
|---|---|---|---|
| 0–1 | 2 | 表版本(uint16) | 0x 00 01→ 1 |
| 2–5 | 4 | 累计脉冲数(uint32) | 0x 00 0B EA 60→ 1234560 |
| 6–7 | 2 | 状态字 | 0x 00 00 |
| 8–15 | 8 | 保留 | 0x 00 … |
主站用 selector = 1、offset_selector{ Offset=0, Count=16 }(即示例 2 的 A-XDR02 0A 06 04 00 00 00 00 12 02 00 10)GETbuffer,拿回 16 字节:
00 01 00 0B EA 60 00 00 00 00 00 00 00 00 00 00按上表解析:表版本=1、累计脉冲数=1234560、状态=0。这就是Utility tables的"搬运 + 主站自解析"模式——DLMS 不解释内部,解析规则由表定义(ANSI/厂商)给出。
示例 7:设备不支持选择性访问时的兜底
若设备buffer的选择性访问未实现(may be available (optional)),主站只能 GET 整个buffer:
- 先 GET
length(短名x + 0x10)确认字节数,避免盲目拉大块; - 若
length在单帧范围内,直接整表 GETbuffer(短名x + 0x18); - 若过大,依赖链路层分片/分段(如 HDLC 的 segmented GET),或退而求其次只取关键区段——但没有选择性访问就不能按 offset/index 精确定位,只能主站侧自己切。
示例 8:用 index_access 逐层解析一张层级表
假设table_ID = 0x0001是一张"ANSI 标准表",内部是 2 层结构:第 1 层是若干记录(record),第 2 层是记录内的字段(field)(以下层级为示例,非蓝皮书原文)。
- 取"第 3 条记录":
index_selector{ Index = [3], Count = 1 }→ 返回第 3 条记录整体; - 取"第 3 条记录的第 2 个字段":
index_selector{ Index = [3, 2], Count = 1 }→ 返回该字段; - 取"第 3 条记录起的整棵子树":
index_selector{ Index = [3], Count = 0 }→Count = 0在请求中表示"从选择点起整棵子树"。
A-XDR 编码Index = [3, 2], Count = 1(array 标签01、2 元素、各long-unsigned标签12;Countlong-unsigned):
02 0E 01 02 12 02 00 03 12 02 00 02 12 02 00 01 (示例:selector=2,Index=[3,2], Count=1)注意Index是数组(即使只一层也要写成单元素数组[3]),不是裸整数;漏写数组包裹会让选择性访问参数结构不符、被设备拒绝。
示例 9:Utility tables 与 Data 类的取舍
两者都装"任意字节",但定位不同(以下对比为工程归纳,非蓝皮书原文):
| 维度 | Utility tables (26) | Data (1) |
|---|---|---|
| 标识 | table_ID(表号)+ logical_name | 仅 logical_name |
| 内部结构 | 不解释,整张表字节 | 单值或结构,类型明确 |
| 适用 | 现成的 ANSI/厂商表 | 自定义的单点/结构数据 |
| 选择性访问 | offset / index(可选) | 无(整属性 GET/SET) |
经验法则:数据已经是某份"标准表/厂商表"定义好的整体 → 用Utility tables;数据是你自己在 DLMS 里新定义的点或结构 → 用Data。别把"Data能装的下就不必用Utility tables"当成铁律——当数据有明确"表号"语义、且要和 ANSI 体系互通时,Utility tables才是正解。
示例 10:主站解析 Utility tables 的通用步骤
(以下流程为工程归纳,非蓝皮书原文)
1. 枚举对象列表,找到 class_id = 26 的实例,记下 logical_name; 2. GET table_ID(x+0x08) → 确认这是哪份表(标准表/厂商表); 3. GET length(x+0x10) → 确认 buffer 字节数,决定整取还是分段; 4. 若设备支持选择性访问: 4a. 按字节取 → selector=1, offset_selector{Offset, Count} 4b. 按层级取 → selector=2, index_selector{Index[], Count} 否则:GET 整个 buffer(x+0x18); 5. 按 table_ID 指向的表定义(ANSI/厂商文档)解析 buffer 字节; 6. 解析规则不在 DLMS 内,主站必须自带"表字典"。核心提醒:Utility tables只负责"搬运",解析规则是主站侧的"表字典"。同一份buffer,不知道table_ID对应的表定义就无从解析——所以第 2 步确认table_ID是整条链路的前提。
5. 工程上容易踩的坑
选择性访问是可选的:原文明确写 “may be available (optional)”。别假设所有设备都支持 selector 1/2;不支持时只能整表 GET,要评估
length会不会撑爆报文。length 是字节数不是元素数:
buffer是裸 octet-string,length计的是字节。想把 buffer 当"行/元素"解析,内部布局必须由表定义(ANSI/厂商)决定,DLMS 不管。index_access 的 Index 是层级路径:
[1, 2]不是"第 1 和第 2 个元素",而是"第 1 层里的第 2 个子节点"。层级语义由表结构决定,乱填会取到错误的子树。Count = 0 在请求中的特殊含义:selector 2 下
Count = 0表示"整棵子树",不是"取 0 个"。别当"空请求"用。table_ID 的编号空间要查清:标准表按 ANSI、厂商表按厂商,编号可能重叠在不同设备语义下。主站解析
buffer前必须先确认table_ID指向哪份表定义,否则字节错位。offset_access 越界:
Offset + Count超过length时行为取决于实现,可能截断或报错。取数前先 GETlength(短名x + 0x10)确认边界。整表过大导致超时:若设备不支持选择性访问且
buffer很大,整表 GET 可能触发长帧/分片或超时。优先用选择性访问分段拉取。offset_selector 的 Offset 是 double-long-unsigned(4 字节):别把
Offset当成 2 字节的long-unsigned去编码。原文结构是Offset: double-long-unsigned,示例 2 用的是 4 字节00 00 00 00;若错编成 2 字节,选择性访问参数会整体错位、被设备拒绝。
6. 小结 & 下期预告
本篇要点:
Utility tables(class_id = 26, version = 0)是与介质无关的数据集装箱,把任意 ANSI/厂商表封装成 COSEM 对象;- 四个属性:
table_ID(表号,long-unsigned)、length(字节数,double-long-unsigned)、buffer(原始字节,octet-string)、logical_name; - buffer 支持可选的选择性访问:selector 1 =
offset_access(偏移+字节数),selector 2 =index_access(层级索引+元素数,Count=0 表整棵子树); - 区别于
Profile generic:它管"别人定义的表",不管"DLMS 自己采的数据"。
下一篇(第 24 篇):Modem configuration(class_id = 27)—— 调制解调器配置。本篇把"非 DLMS 原生表数据"装进了集装箱;下一篇回到"通信外设配置"主线,讲Modem(调制解调器)怎么在 COSEM 里建模——它同样是个"让设备通过 PSTN/蜂窝等拨号网络接入主站"的通信配置类,和前面的端口类(HDLC / 载波双绞线 / M-Bus)形成"不同通信外设"的系列对照。
参考资料:DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》,Utility tables(class_id = 26, version = 0) 章节。文中属性名、数据类型、Short name 偏移、offset_selector / index_selector 结构与引文均与原文一致;示例中的 OBIS、table_ID、字节、A-XDR 编码为帮助理解而构造(实际以设备对象列表为准)。