由于 DLT645 功能配置内容较多,这里单独做一个使用说明;
本文档重点说明配置项作用、上下行数据格式、模板模式以及常见配置案例。
DLT645 网关功能支持对电表、水表、热量表等设备数据进行自动采集、转换,并直接上传 JSON 到服务器;
通用 DLT645 JSON 采集上传指南视频介绍: [在线观看]
最新版本现在已经支持整点时间采集,自动对齐到基站时间 0s
如果读取设备过程中,某一条采集指令无回复或者数据错误,会重复读取 3 次;若 3 次都错误,DTU 上传时会忽略该指令对应的字段
关于数据源的说明:定义数据源是 DLT645 模块输出结果的名称,需要唯一选择,不要和其他功能的定义重复;输入数据源则是从机数据来源,可以是串口、网络、其他自定义,可以多选;要上传 DLT645 的结果,一般情况下网络通道数据源请选当前 DLT645 设置的输出数据源
模板模式输入的不是普通 Lua 脚本,而是 3 个匿名函数;保存时会先做编译和试运行校验,因此模板中必须注意语法正确和字段判空
模板模式输入的不是普通 Lua 脚本,而是 3 个匿名函数;保存时会先做编译和试运行校验,因此模板中必须注意语法正确和字段判空
广播抄读只适合总线上只有一块表响应的场景;如果多块表同时响应,结果不可控
| 属性 | 说明 | 必填 |
|---|---|---|
| 读取超时 | 串口或网络数据返回超时,单位秒 | 是 |
| 输出数据源 | 定义当前功能的数据源名称,需要唯一;用于其他模块如网络配置的数据源选择 | 是 |
| 输入数据源 | 当前功能的数据来源,可以多选,一般是串口、网络、自定义数据源 | 是 |
| 用户参数 | 在结果里面携带用户参数;用户参数支持魔法值:${ts} 时间戳,${date} 格式时间字符串,${imei},${iccid},${csq},${vbat},${ip},${lon},${lat},${alt},${geo},${pmid} 表 ID |
否 |
| 时间戳 | 将会在数据里面插入 ts 时间戳 |
是 |
| 数据结构 | 混合模式、设备模式、模板模式;模板模式支持简易语法构造自定义数据格式 | 是 |
| 指令间隔 | 一条指令读写完后等待延迟的时间,防止太快了设备反应不过来 | 是 |
| 唤醒长度 | 唤醒码 FE 的数量 |
是 |
| 整点对齐 | 采集周期自动对齐到 0s;常用于整分、整点采集 |
是 |
| 重试次数 | 读取失败后的重试次数;当前脚本最少会按 3 次执行 | 否 |
| 红外载波 | 某些红外场景可启用 38K 载波 | 否 |
| 属性 | 说明 | 必填 |
|---|---|---|
| 周期 | 当前采集设备的读取周期,单位秒 | 是 |
| 单独上传 | 勾选后,此设备采集成功即单独上传;其他没有勾选的设备可以组成一条 JSON | 是 |
| 变化上传 | 读取的数据和上次比较,发生变化则上传;多条指令有一条变化也会上传 | 是 |
| 协议版本 | 可选 DLT645 97/07、DLT698、CJ/T188 | 是 |
| 表地址 | 设备地址;单表广播抄读可使用 AAAAAAAAAAAA |
是 |
| 属性 | 说明 | 必填 |
|---|---|---|
| 键值 | 数据映射结果键值(别名),上传 JSON 使用的名称 | 是 |
| 命令(功能码) | 采集命令;如果没有可选项,可以勾选自定义手动输入十六进制命令码 | 是 |
| 比例 | 数据转换,公式为 y = ax + b,a 是比例 |
是 |
| 偏移 | 数据转换,公式为 y = ax + b,b 是偏移 |
是 |
| 自定义 | 如果没有可选的采集命令,可以勾选自定义并输入十六进制命令码 | 否 |
常见命令码为 4 字节 DI,例如:
02010100020201000203000005060001常见命令码一般为 2 字节,例如:
B611B621B630常见格式为 4 字节 OAD,例如:
4000020120000200CJ/T188 一般按表类型统一抄读,由映射决定取第几个返回值。
DLT645 结果支持 3 种数据结构;一般情况下,以及自适应对接内置平台格式时,请选择 混合模式;想要自定义上传格式请选择 模板模式。
所有映射的键值全局唯一,结果将是一个只有一层的 JSON Map
设置两条映射,输出结果:
{"va":220.1,"ia":5.12,"power":1.23}
如果启用了时间戳和用户参数,结果可能是:
{
"va":220.1,
"ia":5.12,
"ts":1712100000,
"imei":"8675xxxxxxxxxxx",
"meter_id":"000001544993"
}
所有映射的键值按设备唯一,结果将是一个有两层的 JSON Array
设置两块表,输出结果:
[
{"i":"000001544993","v":{"va":220.1,"ia":5.12}},
{"i":"000001544995","v":{"va":221.3,"ia":4.98}}
]
i表示表 ID,v表示映射结果
需量键值 + "_ts";示例:
若设置键值为 de
采集结果为:
{"de":123.45,"de_ts":24040130}
示例:
若设置键值为 fryall
采集结果为:
{"fryall":123.12,"fryall_1":100.00,"fryall_2":10.00,"fryall_3":8.00,"fryall_4":5.12}
某些命令返回非标内容,映射取值可以直接得到原始 Hex 字符串:
{"old":"000003A600079E920007A238010000"}
这种场景适合在模板模式里自行解析。
当前脚本推荐使用下面格式下发主动抄读。
{"devid":"000001544993","tab":["va","ia"]}
或者:
{"id":"000001544993","tab":["va","ia"]}
或者支持 msgid:
{"devid":"000001544993","tab":["va","ia"],"msgid":"read001"}
说明:
devid、id、i 都可以识别为设备 ID;tab 表示要读取的属性列表;msgid,返回时也会带回;当前版本请优先使用
tab,不要直接依赖read;如果平台已经固定使用read,建议通过模板模式做一次转换
模板模式下也可以使用数组形式:
[
{"devid":"000001544993","tab":["va","ia"]},
{"devid":"000001544995","tab":["va","ia"]}
]
当前 DLT645 支持 2 种控制方式:
hz 数据体;推荐优先使用“特殊映射键”。
hz 数据体{"devid":"000001544993","hz":"35444444333333334D334A8453433556","msgid":"w001"}
{"devid":"2016022601","hz":"2A04A0170055","msgid":"w002"}
hz是十六进制字符串,不需要自己拼完整帧;脚本会补成完整写帧
旧文档里的
{"hz":"1"}/{"hz":"0"}不建议继续作为通用格式使用;当前版本更推荐用“特殊映射键”
你可以把某个映射配置成“控制类命令”,这样平台只需发简单 JSON。
支持的特殊命令:
FFFFFF01:标准 645 开合闸FFFFFF02:校时FFFFFF03:汉光开合闸FFFFFF04:购电假设配置:
swFFFFFF01下发分闸:
{"devid":"000001544993","sw":0,"msgid":"trip001"}
下发直接合闸:
{"devid":"000001544993","sw":1,"msgid":"close001"}
下发合闸允许:
{"devid":"000001544993","sw":2,"msgid":"allow001"}
假设配置:
hgswFFFFFF03下发:
{"devid":"000001544993","hgsw":1,"msgid":"hg001"}
假设配置:
syncFFFFFF02使用当前系统时间校时:
{"devid":"000001544993","sync":1,"msgid":"ts001"}
使用指定时间戳校时:
{"devid":"000001544993","sync":1712100000,"msgid":"ts002"}
假设配置:
buyFFFFFF04简单写法:
{"devid":"000001544993","buy":100,"msgid":"buy001"}
完整写法:
{
"devid":"000001544993",
"buy":{
"amount":100,
"userNumber":"000000000001",
"purchaseCount":2,
"timestamp":1712100000,
"alarmAmount":30,
"cutoffAmount":10,
"stockLimit":99999,
"creditLimit":0,
"powerLimit":88000,
"limitMode":0
},
"msgid":"buy002"
}
当前版本购电功能建议现场联调验证,不同固件版本底层实现可能存在差异
单表 的情况下可以使用广播地址抄读,同时解析真实表 ID。
配置示例:
AAAAAAAAAAAA${pmid} 获取真实表号;例如:
meter_id = ${pmid}
采集结果示例:
{"va":220.1,"meter_id":"000001544993"}
广播抄读只适合总线上只有一块表响应的场景
数据结构切换到模板模式后,需要设置上下行模板,也可以设置回复模板。
输入格式:
function(tm,td,ex) return tm end
,
function(sub,ex) return sub end
,
function(reply,ex) return reply end
新增的第三个回复模板是从较新版本开始支持
模板输入不是普通 Lua 文件,而是 3 个匿名函数表达式。
正确写法:
function(...) ... end,
function(...) ... end,
function(...) ... end
不建议写成:
local a = 1
function(...) ... end
当前脚本在保存模板后会:
所以模板必须保证:
这是模板模式最重要的注意事项。
因为实际运行时:
td 可能为空;推荐写法:
(tm.a or 0)
if not td or #td < 1 then return {} end
不推荐直接写:
tm.c = tm.a + tm.b
原有属性做判空处理是必须的,否则模板可能在校验阶段或运行阶段报错
-- 输入参数分别是:
-- tm = 混合结构 table
-- td = 设备模式结构 table
-- ex = 用户参数和附加参数
-- 返回一个 table 或字符串
function(tm,td,ex)
return tm
end
-- 输入参数分别是:
-- sub = 下行的数据 table
-- ex = 用户参数
-- 返回可以是 4 个值:
-- 第一个: table 结果
-- 第二个: bool,代表是否主动读取,而不写入
-- 第三个: 串口命令,会将数据直接发到串口
-- 第四个: msgid,用于标记消息
function(sub, ex)
return sub, read, nil, msgid
end
-- 输入参数分别是:
-- reply = 回复 table
-- ex = 用户参数(如果有 msgid,也会在 ex 里面)
-- 645 reply 的属性一般为 { suc = suc, add = add }
function(reply,ex)
return reply
end
这里对属性
(tm.a or 0)和(tm.b or 0)实际上就是判空处理,不存在这个属性就默认赋值为 0,必须对原有属性做判空处理
function(tm,td,ex)
tm.c = (tm.a or 0) + (tm.b or 0)
return table.merge(tm, ex)
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
若原始上传:
{"a":10,"b":11}
模板处理后上传:
{"a":10,"b":11,"c":21}
function(tm,td,ex)
local template = {
version = "1.0",
type = "variant_data",
ts = os.time(),
time = 0,
params = {}
}
tm.c = (tm.a or 0) + (tm.b or 0)
template.params = table.merge(tm, ex)
return template
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
输出示例:
{
"version":"1.0",
"type":"variant_data",
"ts":1712100000,
"time":0,
"params":{"a":10,"b":11,"c":21}
}
function(tm,td,ex)
if not td or #td < 1 then return {} end
local template = {}
for index, value in ipairs(td) do
local id = value.i
template[id] = value.v
end
return template
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
输出结果:
{
"000001544993":{"va":220.1,"ia":5.12},
"000001544995":{"va":221.3,"ia":4.98}
}
function(tm,td,ex)
if not td or #td < 1 then return {} end
return {
msgtype = "test2",
data = td
}
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
输出示例:
{
"msgtype":"test2",
"data":[
{"i":"000001544993","v":{"IA":12,"Temp":10.2}},
{"i":"000001544995","v":{"IA":13,"Temp":10.0}}
]
}
用户参数设置:
000001544993 = device1
000001544995 = device2
模板:
function(tm,td,ex)
if not td or #td < 1 then return {} end
local devId = tostring(td[1].i or "")
return {
DeviceName = ex[devId] or "defaultname",
Timestamp = os.time() * 1000,
DataMap = tm
}
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
若服务器下发数据:
{"devid":"000001544993","read":["va","ia"],"msgid":"r001"}
这里需要把平台格式:
{"devid":"000001544993","read":["va","ia"]}
转换成脚本识别的:
{"devid":"000001544993","tab":["va","ia"]}
使用下行模板:
function(tm,td,ex)
return table.merge(tm, ex)
end,
function(sub,ex)
return {
devid = sub.devid,
tab = sub.read
}, true, nil, sub.msgid
end,
function(reply,ex)
return reply
end
read 字段决定是读取还是写入function(tm,td,ex)
return table.merge(tm, ex)
end,
function(sub,ex)
return sub.data, sub.read, nil, sub.msgid
end,
function(reply,ex)
return reply
end
下发读取:
{"msgid":123,"data":{"AI1":0},"read":true}
下发写入:
{"msgid":124,"data":{"DO1":1},"read":false}
上行目标格式:
{
"devid":"861241057766154",
"ts":"2024-08-10 22:08:15",
"data":{
"000001544993":{"AI1":220,"AI2":10}
},
"msgid":"12455"
}
完整模板:
function (tm, td, ex)
if not td or #td < 1 then return {} end
local imei = misc.getImei()
local list = {}
for index, value in ipairs(td) do
local idx = tostring(value.i)
local tab = list[idx] or {}
list[idx] = table.merge(tab, value.v or {})
end
return {
devid = imei,
ts = os.date("%Y-%m-%d %H:%M:%S"),
data = list,
msgid = ex and ex.msgid
}
end,
function (sub, ex)
return sub.data, sub.read, nil, sub.msgid
end,
function (reply, ex)
return {
stat = reply.suc or false,
msgid = ex and ex.msgid,
prop = reply.prop or "null"
}
end
function (tm, td, ex)
if not td or #td < 1 then return {} end
local template = {
tid = misc.getImei() .. tostring(os.time()),
sendTp = tostring(os.time()),
tp = tostring(os.time()),
pt = {
{
id = misc.getImei(),
product = "DAQ-IR-1",
meterSn = td[1] and td[1].i,
data = td[1] and td[1].v or {}
}
}
}
return template
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
例如某设备返回:
000003A600079E920007A238010000
模板:
function(tm,td,ex)
local hexval = tm.old
if hexval then
local v1, v2, v3, v4 = string.unpack(">LLLxH", hexval)
tm.v1 = v1 * 0.01
tm.v2 = v2 * 0.01
tm.v3 = v3 * 0.01
tm.v4 = v4
tm.old = nil
end
return tm
end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
如果通过 userapp.lua、规则引擎或者自定义脚本去接 DLT645 数据,最常见的是读取 D_RECV_24 (自定义4)消息。
实际收到的内容通常是:
{
b = {va = 220.1, ia = 5.12},
d = "{\"va\":220.1,\"ia\":5.12}",
from = "D6_SEND"
}
字段说明:
b:Lua table 结果;d:JSON 字符串结果;from:数据来源,DLT645 为 D6_SEND;如果是写入或控制回复,还会有:
{
b = {...},
from = "D6_REPLY",
reply = true
}
因此二次开发里一般这样用:
msg.bmsg.dmsg.reply配置:
va -> 02010100ia -> 02020100结果:
{"va":220.1,"ia":5.12}
建议:
结果:
[
{"i":"000001544993","v":{"va":220.1,"ia":5.12}},
{"i":"000001544995","v":{"va":221.3,"ia":4.98}}
]
平台下发:
{"devid":"000001544993","tab":["va","ia"],"msgid":"read001"}
设备即时返回:
{"va":220.1,"ia":5.12,"msgid":"read001"}
配置:
AAAAAAAAAAAAmeter_id=${pmid}结果:
{"va":220.1,"meter_id":"000001544993"}
配置一条特殊映射:
swFFFFFF01平台下发:
{"devid":"000001544993","sw":0,"msgid":"trip001"}
模板:
function(tm, td, ex)
if not td or #td < 1 then return {} end
return {
tid = misc.getImei() .. tostring(os.time()),
sendTp = tostring(os.time()),
pt = {
{
id = misc.getImei(),
meterSn = td[1] and td[1].i,
data = td[1] and td[1].v or {}
}
}
}
end,
function(sub, ex) return sub end,
function(reply, ex) return reply end
tab推荐:
{"devid":"000001544993","tab":["va","ia"]}
hz=1/0 不建议继续当通用格式推荐使用“特殊映射键”做开合闸,下发更清晰、更稳定。
否则最终上传格式可能不是你想要的结果。
推荐:
tm.c = (tm.a or 0) + (tm.b or 0)
不推荐:
tm.c = tm.a + tm.b
推荐直接按下面格式写:
function(tm,td,ex) return tm end,
function(sub,ex) return sub end,
function(reply,ex) return reply end
总线上如果有多块表同时响应,结果不可控。
如果是普通使用者,推荐这样选:
tab;AAAAAAAAAAAA + ${pmid};