入门:读一次电表时钟
理解 DLMS 最快的办法,是亲手读一个值。本文只做一件事:连上电表,读出它的时钟。
全过程只有两问两答:
客户端 电表
| ---------- AARQ(我想建立连接)------> |
| <--------- AARE(同意)--------------- |
| ---------- GET(读时钟)-------------> |
| <--------- 响应(当前时间)----------- |
为了最短路径,本文做了三个简化:
| 选择 | 省掉了什么 |
|---|---|
| 走 TCP(Wrapper) | HDLC 的帧、地址和 CRC |
| 公共客户端,无认证 | 密钥和加密 |
| 读时钟 | 负荷曲线的分块传输和筛选 |
如果还不了解整体分层,可以先看架构、分层与历史。
看报文前的准备
本文的报文都用十六进制书写,两个字符是一个字节。下面四个概念足够看懂全文。
1. 十六进制换算。 0x10 是十进制 16,0x1F 是 31,0x0C 是 12。两个字节合起来时高位在前,比如 07 EA 是 7 × 256 + 234 = 2026。
2. APDU。 应用层的一条完整消息,AARQ、AARE、GET 请求和响应都是 APDU。
3. Wrapper 头。 TCP 上每条 APDU 前面固定加的 8 字节,用来说明"谁发给谁、后面有多长"。
4. 两种编码。 本文会遇到两种写法,分清它们,报文就不再是天书:
| 编码 | 用在哪里 | 特点 | 例子 |
|---|---|---|---|
| BER | AARQ、AARE 的外层 | 每项都是"标签 + 长度 + 值"(TLV),可以逐层拆开 | 02 01 00:02 表示整数,01 表示长度 1 字节,00 是值 |
| A-XDR | xDLMS,包括 GET | 字段按固定顺序排列,长度固定的字段不写标签和长度,更省字节 | 类 ID 就是两个字节 00 08,前面什么都没有 |
第一步:要读什么
DLMS 里读任何数据,都需要三个信息:
| 信息 | 值 | 含义 |
|---|---|---|
| 类 ID | 8 | Clock 接口类 |
| OBIS | 0-0:1.0.0.255 | 这块表上的"时钟"对象 |
| 属性 | 2 | Clock 的 time 属性(属性 1 是对象名) |
这三个数就是 DLMS 的核心。 读电量、读电压,也只是把这三个数换掉。
可以类比成面向对象编程:
| 概念 | 是什么 | 为什么需要 | 编程类比 |
|---|---|---|---|
| 接口类(IC) | 规范预先定义好的一种"数据模板",用数字编号,比如 Clock 是 8 | 所有厂商的时钟结构一致,客户端写一次解析逻辑就够了 | class Clock |
| 对象 | 电表里按某个接口类创建出来的一份实例 | 一块表可能有多个同类对象,比如很多个 Register | new Clock() |
| OBIS | 对象的名字,由 6 个数字组成 | 不管哪个厂商,同一个名字都代表同一种数据 | 变量名 |
| 属性 | 对象里的一项数据,从 1 开始编号 | 一个对象不止一个值:Clock 既有当前时间,也有时区 | 成员字段 |
第二步:建立连接
电表不接受陌生请求,要先建立应用连接。TCP 连到电表(默认端口 4059)后,发送一条 AARQ。报文里的词比较多,先逐个弄清楚。
谁和谁在通信
应用连接(Application Association)
- 是什么:客户端和电表之间的一次"会话约定",约定双方用什么规则对话、客户端有什么权限。
- 为什么需要:TCP 连接只说明网络通了,不说明你是谁、能做什么。电表要靠应用连接来区分权限,比如公共客户端只能读少量数据,管理客户端才能改参数。
客户端地址和公共客户端
- 是什么:地址(规范里叫 SAP,服务访问点)用一个数字表示通信的一方。客户端地址 16 是规范保留给公共客户端的。
- 为什么需要:不同地址对应不同权限。公共客户端不需要密码,让任何人都能先读一点基本信息,比如时钟和帧计数器。
逻辑设备
- 是什么:一块物理电表内部可以分成多个"虚拟电表",每个就是一个逻辑设备,有自己的地址。
- 为什么需要:比如计量部分和通信模块可以分开管理。地址 1 是管理逻辑设备,规范要求每块表都必须有,所以入门时发给它一定没错。
AARQ 和 AARE
- AARQ(Application Association Request):客户端发出的"我想建立连接"。
- AARE(Application Association Response):电表的回答,里面写明同意还是拒绝。
- ACSE(关联控制服务元素):负责建立和释放连接的那部分协议,AARQ 和 AARE 就属于它。它是 ISO 的通用标准,并不是 DLMS 专用的。
- xDLMS:DLMS 专有的那部分协议,负责 GET、SET 这些实际操作。
分工可以这样记:ACSE 负责"接通电话",xDLMS 负责"通话内容"。
应用上下文和 OID
应用上下文(Application Context)
- 是什么:这次对话采用的规则。DLMS 定义了 4 种,由 OID 的最后一个数字区分:
| 最后一位 | 访问方式 | 是否加密 |
|---|---|---|
| 1 | LN(逻辑名) | 否 |
| 2 | SN(短名) | 否 |
| 3 | LN(逻辑名) | 是 |
| 4 | SN(短名) | 是 |
- 为什么需要:连接时先说好规则,电表不支持就当场拒绝,不用等到后面解析报文时才出错。
LN 和 SN
- LN(Logical Name,逻辑名):用 6 字节的 OBIS 编码来指定对象,含义清晰,是现在的主流。
- SN(Short Name,短名):用 2 字节的短地址指定对象,更省字节,但每块表的地址可能不同,是早期低速链路留下的方式。
OID(Object Identifier,对象标识符)
- 是什么:国际上通用的一种"全球唯一编号",像一棵树,每个数字往下走一层。它不是 DLMS 发明的,在 HTTPS 证书等很多领域也都在用。
- 为什么需要:ACSE 是通用标准,需要用一种全球不会重复的方式来表示"我用的是哪套规则"。
本文的 2.16.756.5.8.1.1 逐层展开:
| 数字 | 含义 |
|---|---|
2 | ISO 和 ITU 联合管理的分支 |
16 | 按国家划分 |
756 | 瑞士(DLMS UA 注册在瑞士) |
5 | 瑞士的组织分支 |
8 | DLMS UA |
1 | 应用上下文 |
1 | LN,不加密 |
报文里写成 60 85 74 05 08 01 01,是 BER 的 OID 压缩写法:前两层 2.16 合成一个字节 60(2 × 40 + 16 = 96),756 因为大于 127,要占两个字节 85 74。
OID 和 OBIS 的区别
两者都是一串数字,但完全不是一回事:
| OID | OBIS | |
|---|---|---|
| 命名的是什么 | 规则和机制,比如"LN 不加密" | 电表里的数据,比如"时钟" |
| 来源 | ISO/ITU 的通用体系 | DLMS/COSEM 专用,IEC 62056-6-1 |
| 长度 | 不固定 | 固定 6 组 |
| 出现在哪里 | 建立连接时(AARQ、AARE) | 操作数据时(GET、SET、ACTION) |
| 类比 | 选择"说哪种语言" | 选择"问哪件事" |
用户信息和 InitiateRequest
用户信息(user-information)
- 是什么:ACSE 在 AARQ 里预留的一个"附加信息"字段,ACSE 自己不解析,原样交给上层。
- 为什么需要:ACSE 是通用协议,不懂 DLMS。DLMS 需要协商功能和报文大小,就把这些内容放进这个字段,搭连接请求的顺风车,省掉一次来回。这就是报文里
BE → 04 → 01一层套一层的原因。
InitiateRequest 和 InitiateResponse
- 是什么:装在用户信息里的 xDLMS 消息,客户端用 InitiateRequest 提出"我要什么",电表用 InitiateResponse 回答"实际给你什么"。
- 为什么需要:不同电表支持的功能和内存大小不同,通信前要先对齐。
InitiateRequest 里的每一项:
| 字段 | 是什么 | 有什么用 | 本例 |
|---|---|---|---|
| 专用密钥(dedicated key) | 只在本次连接使用的临时加密密钥 | 用临时密钥加密业务数据,减少长期密钥的暴露 | 不加密,所以不带 |
| response-allowed | 客户端是否希望电表回复这条请求 | 广播场景可以要求电表不回复,避免信道拥塞 | 默认值,需要回复 |
| 服务质量(quality of service) | 早期规范留下的字段,用来协商通信质量等级 | 现在基本不用,但格式上保留了这个位置,所以要填"不带" | 不带 |
| DLMS 版本 | 协议的版本号 | 双方确认能互相理解,目前主流是 6 | 6 |
| 一致性块(conformance block) | 一组开关,声明要用哪些功能 | 见下文 | 只开 GET |
| 最大 PDU | 能接收的单条消息的最大字节数 | 见下文 | 65535 |
一致性块和功能位
- 是什么:一个 24 位的开关表,每一位代表一项功能,1 表示使用,0 表示不用。下面是几个常见的位:
| 位 | 功能 |
|---|---|
| 11 | GET 分块传输 |
| 14 | 一次请求多个对象 |
| 19 | GET(读) |
| 20 | SET(写) |
| 21 | 选择性访问 |
| 22 | 事件通知 |
| 23 | ACTION(执行方法) |
- 为什么需要:电表能力差别很大。客户端列出想用的功能,电表返回双方都支持的部分,之后双方就只用这些功能,不会发出对方不懂的请求。
- 位的编号从左边开始,
00 00 10的二进制是00000000 00000000 00010000,从第 0 位数到第 19 位正好是那个1。
PDU 和最大 PDU
- 是什么:PDU(协议数据单元)就是一条完整的消息。最大 PDU 是一方能接收的单条消息上限。
- 为什么需要:电表内存很小,发送方不能发超过对方上限的消息。数据太长时,要用分块传输拆成多条。
AARE 里多出来的内容
- 连接结果:
00接受,01永久拒绝,02暂时拒绝。这是判断连接成败的唯一依据。 - 诊断信息:拒绝时用来说明原因,比如认证失败或应用上下文不支持,方便排错。
- VAA 名称:VAA 是"虚拟应用关联",早期规范的概念,这个字段告诉客户端用哪个名字访问连接对象。使用 LN 时固定为
0007,收到后不用处理。
报文拆解
客户端发出:Wrapper 头 + AARQ
完整报文,共 39 字节:
00 01 00 10 00 01 00 1F
60 1D A1 09 06 07 60 85 74 05 08 01 01
BE 10 04 0E 01 00 00 00 06 5F 1F 04 00 00 00 10 FF FF
第 1 行是 Wrapper 头:
| 字节 | 字段 | 含义 |
|---|---|---|
00 01 | 版本 | 固定为 1 |
00 10 | 源地址 | 16,表示公共客户端,不需要密码 |
00 01 | 目的地址 | 1,表示电表的管理逻辑设备 |
00 1F | 长度 | 后面的 APDU 有 31 字节 |
第 2 行是 AARQ 的开头,说明要用哪种"对话规则":
| 字节 | 字段 | 含义 |
|---|---|---|
60 | 标签 | 这是一条 AARQ(连接请求) |
1D | 长度 | AARQ 的内容有 29 字节 |
A1 | 标签 | 接下来是应用上下文名称 |
09 | 长度 | 内容有 9 字节 |
06 | 标签 | 类型是对象标识符(OID) |
07 | 长度 | OID 有 7 字节 |
60 85 74 05 08 01 01 | OID 值 | 即 2.16.756.5.8.1.1。开头是 DLMS UA 的编号,最后的 1 表示按逻辑名(LN)访问、不加密 |
第 3 行是用户信息,告诉电表客户端的能力:
| 字节 | 字段 | 含义 |
|---|---|---|
BE | 标签 | 接下来是用户信息 |
10 | 长度 | 内容有 16 字节 |
04 | 标签 | 类型是字节串 |
0E | 长度 | 字节串有 14 字节,里面就是 InitiateRequest |
01 | InitiateRequest 标签 | 从这里开始是 xDLMS 的初始化请求 |
00 | 专用密钥 | 不带 |
00 | response-allowed | 使用默认值:需要响应 |
00 | 服务质量 | 不带 |
06 | DLMS 版本 | 版本 6 |
5F 1F | 标签 | 接下来是一致性块,即客户端想用的功能 |
04 | 长度 | 内容有 4 字节 |
00 | 未用位数 | 后面 3 字节的 24 位全部有效 |
00 00 10 | 功能位 | 只有第 19 位为 1,表示 GET。本例只需要这一个功能 |
FF FF | 最大 PDU | 客户端最多能接收 65535 字节 |
电表返回:Wrapper 头 + AARE
00 01 00 01 00 10 00 2B
61 29 A1 09 06 07 60 85 74 05 08 01 01
A2 03 02 01 00
A3 05 A1 03 02 01 00
BE 10 04 0E 08 00 06 5F 1F 04 00 00 00 10 04 00 00 07
第 1 行是 Wrapper 头,源和目的地址与请求相反:
| 字节 | 字段 | 含义 |
|---|---|---|
00 01 | 版本 | 固定为 1 |
00 01 | 源地址 | 1,电表 |
00 10 | 目的地址 | 16,公共客户端 |
00 2B | 长度 | 后面的 APDU 有 43 字节 |
后面 4 行是 AARE。最重要的只有第 3 行:
| 字节 | 字段 | 含义 |
|---|---|---|
61 | 标签 | 这是一条 AARE(连接响应) |
29 | 长度 | 内容有 41 字节 |
A1 09 06 07 60 85 74 05 08 01 01 | 应用上下文 | 电表确认使用同样的规则,结构与请求相同 |
A2 03 02 01 00 | 连接结果 | A2 是结果标签,03 是长度,02 01 表示 1 字节整数,00 表示接受 |
A3 05 A1 03 02 01 00 | 诊断信息 | A3 是诊断标签,05 是长度,A1 03 表示来自 ACSE 用户,02 01 00 表示没有问题 |
BE 10 04 0E | 用户信息外层 | 与请求含义相同 |
08 | InitiateResponse 标签 | 从这里开始是 xDLMS 的初始化响应 |
00 | 服务质量 | 不带 |
06 | DLMS 版本 | 版本 6 |
5F 1F 04 00 | 一致性块头 | 含义与请求相同 |
00 00 10 | 协商后的功能 | 双方都支持的功能:GET |
04 00 | 电表最大 PDU | 电表最多能接收 1024 字节 |
00 07 | VAA 名称 | 使用 LN 访问时固定为 0007 |
第三步:发 GET
连接建立后,把第一步的三个数填进请求。这条报文里有几个新词:
| 术语 | 是什么 | 为什么需要 |
|---|---|---|
| GET | 读取对象属性的服务。对应的还有 SET(写属性)和 ACTION(执行方法,比如拉合闸) | DLMS 对数据的操作只有这几种,非常规整 |
| Normal | GET 的一种形式:一次读一个属性 | 另外还有 With-List(一次读多个)和 Next(继续取下一块数据) |
| 调用 ID(invoke ID) | 客户端给请求打的编号,0 到 15 | 电表原样带回,客户端靠它把响应和请求对上号 |
| 优先级 | 分高和普通两档 | 支持优先级的电表会先处理高优先级请求 |
| 需要确认(confirmed) | 要求电表必须回复 | 不需要确认的请求适合广播,比如同时下发校时 |
| 选择性访问 | 只读属性的一部分 | 负荷曲线可能有几万条,可以只读某个时间段。时钟只有一个值,用不上 |
编号、优先级和确认方式都压缩在一个字节 C1 里,以节省流量。
00 01 00 10 00 01 00 0D
C0 01 C1 00 08 00 00 01 00 00 FF 02 00
第 1 行的 Wrapper 头与 AARQ 的相同,只有长度变成 00 0D(13 字节)。
第 2 行是 GET 请求,用的是 A-XDR,字段按顺序排列:
| 字节 | 字段 | 含义 |
|---|---|---|
C0 | 服务 | GET 请求 |
01 | 类型 | Normal,读单个属性 |
C1 | 调用 ID 和优先级 | 二进制 1100 0001:最高位 1 表示高优先级,次高位 1 表示需要确认,低 4 位表示调用 ID 为 1 |
00 08 | 类 ID | 8,Clock |
00 00 01 00 00 FF | OBIS | 0-0:1.0.0.255,每字节对应 OBIS 的一组 |
02 | 属性 | 2,time |
00 | 选择性访问 | 不使用 |
加粗的三项,就是第一步说的三个数。
第四步:读懂响应
先看几个术语:
| 术语 | 是什么 | 为什么需要 |
|---|---|---|
| 数据类型标签 | 值前面的一个字节,说明值的类型,比如 09 是字节串 | 属性的类型可能不同,接收方要靠它知道如何解析。注意这里和 GET 请求不同,返回的数据是带类型的 |
| 字节串(octet-string) | 一段原始字节,前面带长度 | 时间没有单独的基本类型,Clock 把 12 字节的时间装在字节串里 |
| 未指定 | 字段值为 FF(两字节字段为 FF FF,偏差是 80 00) | 电表不提供该值时用它占位,这样 12 字节的结构不会变 |
| 偏差(deviation) | 本地时间与 UTC 相差的分钟数 | 只有知道时区,才能把电表时间换算成统一的时间 |
| 时钟状态 | 一个按位表示的字节 | 告诉你这个时间是否可信:比如第 0 位表示时间无效,第 1 位表示时间存疑,第 7 位表示正处于夏令时 |
报文如下:
00 01 00 01 00 10 00 12
C4 01 C1 00 09 0C
07 EA 0B 08 07 10 1E 00 FF 80 00 00
第 1 行的 Wrapper 头:00 01 版本,00 01 来自电表,00 10 发给公共客户端,00 12 表示后面有 18 字节。
第 2 行是响应头:
| 字节 | 字段 | 含义 |
|---|---|---|
C4 | 服务 | GET 响应 |
01 | 类型 | Normal |
C1 | 调用 ID 和优先级 | 与请求相同,用来配对请求和响应 |
00 | 结果 | 成功,后面是数据。如果是 01,后面跟错误码 |
09 | 数据类型 | 字节串(octet-string) |
0C | 长度 | 12 字节 |
第 3 行的 12 字节就是时间:
| 字节 | 字段 | 含义 |
|---|---|---|
07 EA | 年 | 2026 |
0B | 月 | 11 |
08 | 日 | 8 |
07 | 星期 | 星期日(1 是星期一,7 是星期日) |
10 | 时 | 16 |
1E | 分 | 30 |
00 | 秒 | 0 |
FF | 百分之一秒 | 未指定 |
80 00 | 与 UTC 的偏差 | 未指定 |
00 | 时钟状态 | 正常 |
结果:2026-11-08 16:30:00。
本节和上一节的 AARE、响应是按规范构造的示例,真实电表协商出的参数和时间会不同。
完整代码
单文件,只用 JDK,Java 8 即可编译。报文常量已按上面的字段分行:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ReadClock {
static final int CLIENT_SAP = 16; // 公共客户端
static final int SERVER_SAP = 1; // 管理逻辑设备
static final byte[] AARQ = hex(""
+ "60 1D" // AARQ,内容 29 字节
+ "A1 09 06 07 60 85 74 05 08 01 01" // 应用上下文:LN,不加密
+ "BE 10 04 0E" // 用户信息外层
+ "01 00 00 00 06" // InitiateRequest,DLMS 版本 6
+ "5F 1F 04 00 00 00 10" // 一致性块:只要 GET
+ "FF FF"); // 客户端最大 PDU
static final byte[] GET_CLOCK = hex(""
+ "C0 01 C1" // GET 请求,Normal,调用 ID 1
+ "00 08" // 类 ID:8(Clock)
+ "00 00 01 00 00 FF" // OBIS:0-0:1.0.0.255
+ "02" // 属性:2(time)
+ "00"); // 不使用选择性访问
public static void main(String[] args) throws IOException {
String host = args[0];
int port = args.length > 1 ? Integer.parseInt(args[1]) : 4059;
try (Socket socket = new Socket(host, port)) {
socket.setSoTimeout(5000);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream());
byte[] aare = exchange(in, out, AARQ);
int result = associationResult(aare);
if (result != 0) {
throw new IOException("电表拒绝连接,association-result = " + result);
}
byte[] response = exchange(in, out, GET_CLOCK);
System.out.println("电表时间:" + parseClock(response));
}
}
static byte[] exchange(DataInputStream in, DataOutputStream out, byte[] apdu)
throws IOException {
out.writeShort(1); // Wrapper 版本
out.writeShort(CLIENT_SAP); // 源
out.writeShort(SERVER_SAP); // 目的
out.writeShort(apdu.length);
out.write(apdu);
out.flush();
System.out.println("-> " + toHex(apdu));
in.readUnsignedShort(); // 版本
in.readUnsignedShort(); // 源
in.readUnsignedShort(); // 目的
byte[] reply = new byte[in.readUnsignedShort()];
in.readFully(reply);
System.out.println("<- " + toHex(reply));
return reply;
}
/** 遍历 AARE 的顶层 TLV,取出 A2 里的结果。本例中的长度都小于 128。 */
static int associationResult(byte[] aare) throws IOException {
if ((aare[0] & 0xFF) != 0x61) {
throw new IOException("不是 AARE");
}
for (int i = 2; i < aare.length; i += 2 + (aare[i + 1] & 0xFF)) {
if ((aare[i] & 0xFF) == 0xA2) {
return aare[i + 4] & 0xFF;
}
}
throw new IOException("AARE 中没有 association-result");
}
static String parseClock(byte[] r) throws IOException {
if ((r[0] & 0xFF) != 0xC4 || r[1] != 0x01) {
throw new IOException("不是 Get-Response-Normal");
}
if (r[3] != 0x00) {
throw new IOException("读取失败,data-access-result = " + (r[4] & 0xFF));
}
if (r[4] != 0x09 || r[5] != 0x0C) {
throw new IOException("不是 12 字节的 octet-string");
}
int year = (r[6] & 0xFF) << 8 | (r[7] & 0xFF);
return String.format("%04d-%02d-%02d %02d:%02d:%02d",
year, r[8] & 0xFF, r[9] & 0xFF, r[11] & 0xFF, r[12] & 0xFF, r[13] & 0xFF);
}
static byte[] hex(String s) {
String h = s.replace(" ", "");
byte[] b = new byte[h.length() / 2];
for (int i = 0; i < b.length; i++) {
b[i] = (byte) Integer.parseInt(h.substring(i * 2, i * 2 + 2), 16);
}
return b;
}
static String toHex(byte[] b) {
StringBuilder sb = new StringBuilder();
for (byte x : b) {
sb.append(String.format("%02X ", x));
}
return sb.toString().trim();
}
}
运行:
javac -encoding UTF-8 ReadClock.java
java ReadClock 192.168.1.100 4059
把地址换成支持 TCP 的电表或 DLMS 模拟器。
实测结果
下面是这段代码读 Gurux DLMS 模拟器(用自带的 crystal.xml 模板,-w 启用 Wrapper)的真实输出。模拟器只作为外部测试对象,上面的代码没有使用它的任何代码。-> 是发出的,<- 是收到的,程序只打印 APDU,不打印 8 字节的 Wrapper 头:
-> 60 1D A1 09 06 07 60 85 74 05 08 01 01 BE 10 04 0E 01 00 00 00 06 5F 1F 04 00 00 00 10 FF FF
<- 61 29 A1 09 06 07 60 85 74 05 08 01 01 A2 03 02 01 00 A3 05 A1 03 02 01 00 BE 10 04 0E 08 00 06 5F 1F 04 00 00 00 10 03 40 00 07
-> C0 01 C1 00 08 00 00 01 00 00 FF 02 00
<- C4 01 C1 00 09 0C 07 E4 06 18 FF 0C 03 33 FF FE B6 00
电表时间:2020-06-24 12:03:51
第 1 行(发出 AARQ): 与第二步的 AARQ 逐字节相同。
| 字节 | 含义 |
|---|---|
60 1D | AARQ,内容 29 字节 |
A1 09 06 07 60 85 74 05 08 01 01 | 应用上下文:LN,不加密 |
BE 10 04 0E | 用户信息外层 |
01 00 00 00 06 | InitiateRequest,DLMS 版本 6 |
5F 1F 04 00 00 00 10 | 一致性块:只要 GET |
FF FF | 客户端最大 PDU:65535 |
第 2 行(收到 AARE): 模拟器接受了连接。
| 字节 | 含义 |
|---|---|
61 29 | AARE,内容 41 字节 |
A1 09 06 07 60 85 74 05 08 01 01 | 应用上下文:LN,不加密 |
A2 03 02 01 00 | 连接结果:00 接受 |
A3 05 A1 03 02 01 00 | 诊断:没有问题 |
BE 10 04 0E | 用户信息外层 |
08 00 06 | InitiateResponse,DLMS 版本 6 |
5F 1F 04 00 00 00 10 | 协商后的功能:GET |
03 40 | 模拟器最大 PDU:832(示例里是 1024) |
00 07 | VAA 名称:LN 固定值 |
第 3 行(发出 GET): 与第三步的 GET 逐字节相同。
| 字节 | 含义 |
|---|---|
C0 01 C1 | GET 请求,Normal,调用 ID 1 |
00 08 | 类 ID:8,Clock |
00 00 01 00 00 FF | OBIS:0-0:1.0.0.255 |
02 | 属性:2,time |
00 | 不使用选择性访问 |
第 4 行(收到时间):
| 字节 | 含义 |
|---|---|
C4 01 C1 | GET 响应,Normal,调用 ID 1 |
00 | 成功 |
09 0C | 字节串,12 字节 |
07 E4 | 年:2020 |
06 | 月:6 |
18 | 日:24 |
FF | 星期:未指定 |
0C | 时:12 |
03 | 分:3 |
33 | 秒:51 |
FF | 百分之一秒:未指定 |
FE B6 | 偏差:-330 分钟(按有符号 16 位解释) |
00 | 时钟状态:正常 |
和前文的示例对比,有三处值得注意:
- 最大 PDU 是协商结果,每块表都可能不同。
- 星期是
FF(未指定)。解析时不能假定每个字段都有值,年、月、日、时、分、秒同样可能是"未指定",上面的简化代码没有处理这种情况。 - 偏差是 -330 分钟,而模板里的时间是 UTC+5:30。也就是说,这个模拟器的符号是"UTC 减本地时间",正是下一节提到的坑。
模板里的时间是静态的,所以读出来是 2020 年。
读不出来时
| 现象 | 常见原因 |
|---|---|
| TCP 连不上或超时 | 地址、端口不对,或电表只开了串口 / HDLC |
| 发出 AARQ 后没有回应 | Wrapper 地址不对,或电表要求 HDLC 成帧 |
AARE 中 A2 03 02 01 后面不是 00 | 电表拒绝连接:不开放公共客户端,或要求认证 |
GET 响应是 C4 01 C1 01 03 | 第 4 个字节 01 表示失败,最后的 03 表示无权读写 |
GET 响应是 C4 01 C1 01 04 | 同样失败,最后的 04 表示对象不存在,检查类 ID 和 OBIS |
读到的时间看起来差了几小时,先检查偏差字段:不同电表对正负号的处理并不一致,所以解析时不要想当然。
接下来
现在你已经走通了 DLMS 的主干。这条链路上,每处复杂度都能找到自己的位置:
- 换对象: 把类 ID、OBIS、属性换成
3、1-0:1.8.0.255、2,读的就是总正向有功电量。注意它的值要配合属性 3 的scaler_unit换算。 - 加认证: 在 AARQ 里加 LLS 或 HLS 字段。
- 加加密: 把 GET 包进加密 APDU。
- 换链路: 把 8 字节 Wrapper 换成 HDLC 帧,APDU 不用改。
- 读长数据: 负荷曲线需要选择性访问和分块传输。