DLMS/COSEM 蓝皮书解读(二):Data 类(class_id = 1)—— 对象模型的"最小完备样本"
系列说明:本系列基于 DLMS UA《Blue Book(蓝皮书)第 16 版 · 第 2 部分》,一个接口类一篇。第 1 篇建立了协议、文档、模型三张地图。本篇进入第一个具体的类。
上篇回顾:第 1 篇讲过 —— 计量设备 = 一堆 COSEM 对象的集合,每个对象是某个接口类的实例,靠 class_id + logical_name + 属性索引定位。本篇就用最简单的类,把这个抽象说法落成具体的样子。
0. 为什么从 Data 类讲起
Data 是蓝皮书里 class_id 最小(= 1)、结构最简单 的接口类:两个属性、零个方法,原文定义不到一页。
但千万别因为它简单就跳过 —— 它是理解整个对象模型的"最小完备样本":把 Data 吃透,你就掌握了四件后面反复用到的事:
一句话定位:
Data = 一个带标准名字的通用数据槽。它不关心数据代表什么物理量、也没有单位 —— 它只负责"用 OBIS 名字标定身份 + 把任意类型的数据装进去"。
1. 它解决什么问题:表计里"不该有单位"的那些东西
先看一台真实电表里的两类数据:
| 正向有功总电能 | 1234,5 kWh | ✅ 必须(否则主站不知道这是度还是瓦时) |
| 设备序列号 | "ABC20230001" | ❌ 不需要(它就是个字符串) |
| 心跳周期配置 | 300(秒) | ❌ 不需要(配置项,主站知道含义) |
| 电池欠压标志 | TRUE | ❌ 不需要(是/否而已) |
| 固件版本 | "FW-v2.3.1" | ❌ 不需要 |
后四类东西,强行给它们配单位不仅多余,还会造成误解("300 秒"的 scaler_unit 该怎么填?)。Data 就是为这类"没有物理量纲"的数据准备的通用容器。
蓝皮书原文(Data, Overview):
“This IC allows modelling various data, such as configuration data and parameters. The data are identified by the attribute logical_name.”
基数(Cardinality):0…n —— 一个逻辑设备里可以有 0 个或多个 Data 对象,数量不限(有些类是强制必现的,Data 不是)。
2. 类蓝图
Data 0…n class_id = 1, version = 0
| logical_name | (static) | octet-string | x | |||
| value | (原文未标 static,即动态) | CHOICE | x + 0x08 |
| Specific methods | 无(该类没有任何方法) |
三点必须记住:
3. 属性一:logical_name(身份证 / OBIS 编码)
- 类型:octet-string,定长 6 字节
- 性质:(static) —— 设备自己不会改它
- 取值:必须符合 OBIS 编码规则
3.1 OBIS 到底怎么读
OBIS 文本形式写作 A-B:C.D.E*F,六段(A/B/C/D/E/F)各占 1 字节,正好编进 6 字节的 logical_name:
| A | 介质(Medium) | 计量的是什么能量 | 0 = 抽象;1 = 电;6 = 热;7 = 气;8 = 水 |
| B | 通道(Channel) | 第几个测量通道 | 0 = 无通道(整体) |
| C | 数据类别 | 测量量 / 数据项类型 | 1 = 有功电能;15 = 有功功率;96 = 厂商/设备标识类;0 = 抽象对象 |
| D | 类型 / 处理方法 | 累计?瞬时?最大值? | 8 = 累计值;7 = 瞬时值;2 = 版本类 |
| E | 费率(Tariff) | 属于哪个费率 | 0 = 总;1/2/3 = 费率 1/2/3 |
| F | 计费周期 | 属于哪个结算周期 | 0 = 当前;255(0xFF)= 不区分 |
3.2 【举例】把 1-0:1.8.0 逐字节拆开
文本:1-0:1.8.0
│ │ │ │ │
│ │ │ │ └─ E = 0 → 总(不分费率)
│ │ │ └─── D = 8 → 累计值
│ │ └───── C = 1 → 有功电能
│ └─────── B = 0 → 无通道(整体)
└───────── A = 1 → 电
语义:正向有功总电能(+A,累计,不分费率)
6 字节编码(十六进制):01 00 01 08 00 00
│ │ │ │ │ └─ F = 0x00
│ │ │ │ └──── E = 0x00
│ │ │ └─────── D = 0x08
│ │ └────────── C = 0x01
│ └───────────── B = 0x00
└──────────────── A = 0x01
再拆一个 Data 类常用的(设备标识):
文本:0-0:96.1.0
A=0 → 抽象对象(非某种能源)
C=96 → 厂商/设备标识类
D=1 → 设备标识(序列号一类)
E=0 → 总
语义:设备序列号(Device ID / 制造序列号)
6 字节编码:00 00 60 01 00 00
注意 C = 96 → 十六进制 0x60
常见的设备标识类 OBIS:0-0:96.1.0、0-b:96.1.0*255 等(具体以设备的对象列表为准)。0x60 = 96。
3.3 为什么 logical_name 必须排第一
因为它是对象的主键。蓝皮书原文(Clause 4):
“The first attribute of any object is the logical_name.”
任何对象的引用,至少要带 class_id + logical_name。主站拿到一个对象的 logical_name,就能知道"这是什么";再配合 class_id,就知道"它有哪些属性可以读"。
4. 属性二:value(万能数据槽,CHOICE)
“Contains the data. The data type depends on the instantiation defined by the logical_name and possibly from the manufacturer. It has to be chosen so, that together with the logical_name, an unambiguous interpretation is possible. Any simple and complex data types listed in … can be used, unless the choice is restricted.”
三层含义:
4.1 简单数据类型(含 A-XDR tag)
| null-data | [0] | 空值 / 占位 | 值不可用 |
| boolean | [3] | TRUE / FALSE | 标志位(Data 专属优势) |
| bit-string | [4] | 位串 | 状态位集合 |
| double-long | [5] | 32 位有符号 | |
| double-long-unsigned | [6] | 32 位无符号 | 大累计量 |
| octet-string | [9] | 字节串 | 二进制、签名、MAC 地址 |
| visible-string | [10] | ASCII 可见字符串 | 序列号、固件版本 |
| utf8-string | [12] | UTF-8 字符串 | 含中文/多字节文本 |
| bcd | [13] | BCD 编码数 | 老式表计数值 |
| integer | [15] | 8 位有符号 | |
| long | [16] | 16 位有符号 | |
| unsigned | [17] | 8 位无符号 | 小配置项 |
| long-unsigned | [18] | 16 位无符号 | 常用配置值 |
| long64 | [20] | 64 位有符号 | |
| long64-unsigned | [21] | 64 位无符号 | |
| enum | [22] | 枚举 | 状态、模式 |
| float32 | [23] | 32 位浮点 | |
| float64 | [24] | 64 位浮点 | |
| date-time | [25] | 日期时间 | 时间戳(Data 专属优势) |
| date | [26] | 日期 | |
| time | [27] | 时间 | |
| delta-integer … delta-double-long-unsigned | [28]–[33] | 差分编码系列 | 压缩存储 |
4.2 复杂数据类型 ⭐(Data 相对 Register 的关键优势)
| array | [1] | 数组(同类型元素序列) |
| structure | [2] | 结构(字段可不同类型、可嵌套) |
| compact-array | [19] | 紧凑数组(类型一致时省 tag) |
这意味着:一个 Data 对象可以把一整条"配置结构"整体存进来。而 Register 的 value CHOICE 里没有这些复杂类型(见第 5 节对比)。
4.3 与 Register 的类型范围对比(重要)
把 Data(本篇)与 Register(第 3 篇)的 value CHOICE 放在一起看:
| null-data / bit-string / 整数族 / 浮点族 / enum | ✅ | ✅ |
| octet-string / visible-string / utf8-string | ✅ | ✅ |
| boolean | ✅ | ❌ |
| bcd | ✅ | ❌ |
| date-time / date / time | ✅ | ❌ |
| delta-* 差分系列 [28]–[33] | ✅ | ❌ |
| array / structure / compact-array | ✅ | ❌ |
| scaler_unit(量纲) | ❌ | ✅ |
结论:
- Data 的 value 是母表(简单 + 复杂全支持);
- Register 的 value 只取了其中数值型的简单类型,但多了一个 scaler_unit。
简单记:要装结构/文本/标志 → Data;要装有单位的数 → Register。
5. 没有方法:Data 与 Register 的分界线
Data 类没有任何 Specific methods。对比 Register 有一个可选方法 reset(data)(把 value 复位到实例特定的默认值)。
语义边界:
| 用途 | 任意数据(配置、参数、字符串、标志、结构…) | 带量纲的过程值 / 状态值 |
| 属性数 | 2(logical_name + value) | 3(多一个 scaler_unit) |
| 是否有单位/倍率 | ❌ | ✅ scaler_unit = { scaler, unit } |
| 方法 | 无 | 可选 reset |
| value 类型范围 | 简单 + 复杂全支持 | 仅数值型简单类型 |
| 典型场景 | 序列号、固件版本、配置项、布尔标志 | 电能、功率、电压、电流 |
蓝皮书给的"退化"口子(第 3 篇会详细展开):如果硬要用 Register 当 Data(scaler_unit 不用,或 scaler = 0 + unit = 255),那么 Register 的 value 可以使用 Data 类允许的全部数据类型。
反过来说:Data 是更底层的通用容器,Register 是在它之上叠加量纲的特化。
6. 【实战举例】
例 1:设备制造序列号
logical_name = 0-0:96.1.0 (示例 OBIS,设备标识类)
value 类型 = visible-string [10]
value 取值 = "ABC20230001"
主站读取:GET ( 1, 0-0:96.1.0, 2 )
响应编码(A-XDR):0A 0B 41 42 43 32 30 32 33 30 30 30 31
│ │ └────── "ABC20230001" 的 ASCII
│ └───────── 长度 11
└──────────── tag = 0x0A (visible-string)
这是最简单的 Data 用法:一个字符串,没有单位,谁读都是这个字符串。
例 2:一个通信配置参数(心跳周期)
logical_name = 某配置项 OBIS
value 类型 = long-unsigned [18]
value 取值 = 300 (心跳周期,单位秒)
主站写入:SET ( 1, <OBIS>, 2, 300 )
响应编码:12 01 2C (0x012C = 300)
注意:"秒"这个概念没有存在对象里,是主站和运维约定好的。这正是 Data 的取舍 —— 换来简单,代价是单位靠约定。
例 3:布尔状态标志(电池欠压)
logical_name = 某状态类 OBIS
value 类型 = boolean [3]
value 取值 = TRUE (电池欠压)
响应编码:03 01
boolean 在 Register 里不可用 —— 所以"是/否"类标志只能(也最好)用 Data。
例 4:固件标识
logical_name = 固件版本类 OBIS(常见如 1-0:0.2.0,以设备为准)
value 类型 = visible-string [10]
value 取值 = "FW-METER-A-v2.3.1"
与之配套的"固件数字签名"通常也是 Data 思路的延伸 —— 把二进制签名塞进 octet-string 类型的 value。
例 5 ⭐:用 structure 一次装下一组配置(Register 做不到)
这是 Data 最能体现"通用"的场景 —— 把一整条配置结构放进一个 value:
logical_name = 某通信配置 OBIS
value 类型 = structure [2]
value 取值 = {
heartbeat_interval : long-unsigned = 300, # [18]
tls_enabled : boolean = TRUE, # [3]
server_address : visible-string = "meter.example.com" # [10]
}
报文里的样子(A-XDR):
02 ← structure 标签
03 ← 3 个元素
12 01 2C ← 元素1: long-unsigned 300
03 01 ← 元素2: boolean TRUE
0A 11 6D 65 74 65 72 … ← 元素3: visible-string "meter.example.com"(长度 0x11 = 17)
主站一次 GET 就拿回整组配置,不需要发三条请求。这就是为什么"复杂类型"的支持如此重要 —— 而 Register 的 value 装不了 structure,只能拆成三个独立对象。
例 6:SN(短名)寻址下怎么读
某 Data 对象的基名 x(示例 0x0100)
logical_name → x = 0x0100
value → x + 0x08 = 0x0108
主站:GET ( short_name = 0x0108 )
对比 LN 寻址要带 6 字节 OBIS + class_id,SN 只发一个 13 位整数 —— 报文更短,适合资源受限设备(如无线水表)。
7. 工程上容易踩的坑
8. 小结 & 下期预告
本篇要点:
下一篇(第 3 篇):Register 类(class_id = 3) —— 在 Data 的基础上加上第三个属性 scaler_unit。你会看到 scaler / unit 如何把裸数字 12345 变成"1 234,5 Wh",为什么蓝皮书说 Register 对象"知道自己所装值的性质",以及"硬编码 scaler 会让你差 10 倍"这个经典事故是怎么发生的。我们还会把物理单位枚举表(unit = 30 是 Wh、27 是 W、255 是 count)一次讲清。
参考资料:DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》,Data (class_id = 1, version = 0) 章节(含 Overview、类蓝图、value 的 CHOICE 类型表)与 Clause 4。文中属性、数据类型与 tag、Short name 偏移、OBIS 结构说明均与该章节原文一致;示例中的 OBIS 取值与配置为帮助理解而构造(实际以设备对象列表为准)。




