由于Modbus功能比较复杂,这里单独做一个使用说明;
更为详细的Modbus全场景应用白皮书 点我查看
Modbus网关功能支持对设备数据的自动读写、转换,直接上传json到服务器;
通用Modbus JSON采集上传指南视频介绍: [在线观看]
最新版本现在已经支持整点时间采集,自动对齐到基站时间0s
如果读取设备过程中,某一条采集指令无回复或者数据错误,会重复读取3次,若3次都错误,DTU上传时会忽略该指令对应的字段
关于数据源的说明:定义数据源是modbus模块输出结果的名称,需要唯一选择,不要和其他功能的定义重复;输入数据源则是从机数据来源,可以是串口、网络、其他自定义,可以多选;要上传Modbus的结果,一般情况下网络通道数据源请选自定义1(就是modbus设置的定义数据源)
设置好Modbus点写入配置的时候页面上提示红色字体映射地址错误: 原因是映射地址必须要包含在指令起始加读取长度里面; 如起始地址1,读取长度2,代表读取的地址是1和2,则只能在地址1、2里面映射。
属性 | 说明 | 必填 |
---|---|---|
读取超时 | 串口数据返回超时,单位秒 | 是 |
协议类型 | 当前配置的协议类型,RTU ,TCP not ok,R2T 代表RTU转TCP,ATCP 代表modbus tcp,数据源来自任意输入 |
是 |
输出数据源 | 定义当前功能的数据源名称,需要唯一,用在其他模块如网络配置的数据源需要选择当前的名称 | 是 |
输入数据源 | 当前功能的数据来源,可以多选,一般是串口,网络,自定义数据源 | 是 |
用户参数 | 在结果里面携带用户参数,用户参数支持魔法值,用户参数名称自己填写,值写这些可以自动替换: ${ts} 时间戳,${date} 格式时间字符串, ${imei} ,${iccid} ,${csq} 动态信号,${vbat} 动态电源电压,${lon} 动态经度(有GPS用gps没有就是基站位置),${lat} 动态纬度,${spd} 动态速度,${fix} 是否定位,${alt} 动态GPS海拔(From V42),${geo} 动态阿里云GPS信息结构体(From V42),${ip} 动态IP |
否 |
时间戳 | 将会在数据里面插入为ts的时间戳 | 是 |
整点对齐 | 采集周期会自动对齐到0s,必须输入10的倍数,采集时间将从00:00开始计时 | 是 |
数据结构 | 混合模式或者设备模式,混合模式下所有采集指令键值会组合成一个json对象(键值必须全局唯一):{“a”:1,“b”:2}, 设备模式下每个设备按照ID排序组成json数组(每个指令下键值唯一):[{“i”:“id1”, “c”:“func”,v:{“a”:1,“b”:2}},{“i”:“id2”, “c”:“func”,v:{“a”:1,“b”:2}}] ; 模板模式支持简易语法构造自定义数据格式,使用比较复杂,后续单独说明 | 是 |
指令间隔 | 一条指令读写完了等待延迟的时间,防止太快了传感器反应不过来 | 是 |
写入回复 | 服务端写入新数据到modbus设备后,是否返回结果 | 否 |
小数位数 | 结果保留的小数位数 | 否 |
变化容差 | 变化上传使用,大于这个这个范围认为数据变化,如果是多个数据,会判断整体差值,不太精准 | 否 |
属性 | 说明 | 必填 |
---|---|---|
周期 | 采集指令读取周期,单位秒 | 是 |
单独上传 | 勾选后,此采集指令读取成功即上传,会组成单独的json;而其他没有勾选的指令会组成一条json | 是 |
变化上传 | 读取的数据和上次比较,发生变化则上传;多条指令有一条发生变化也会上传 | 是 |
只写 | 当数据只需要写时,需要设置为只写 ,支持功能码05、06, 此时读取周期无效 |
是 |
写功能码 | 写指令转换使用的的功能码,寄存器写入用06 还是10 |
是 |
ID | 采集指令,Modbus 设备ID, 最大是255 | 是 |
功能 | 采集指令,Modbus 设备功能码,支持01 、02 、03 、04 、05 、06 |
是 |
地址 | 采集指令,Modbus 设备寄存器起始地址,十进制数字,协议地址是从0开始的,如果是PLC地址要减40001 | 是 |
长度 | 采集指令,Modbus 设备寄存器读写长度,必须包含 所有映射中的地址 |
是 |
键值 | 数据映射,寄存器键值(别名),英文字符串, 转换成json使用的名称 | 是 |
地址 | 数据映射,寄存器地址,必须包含 在采集指令的采集范围中;包含 的**计算方法,映射的地址要大于起始地址,小于等于起始地址加指令长度 |
是 |
类型 | 数据映射,寄存器值类型,和modbus poll软件格式一致,支持Coils, Short,Unsigned Short,Long,Unsigned Long,Float,Short BCD,Long BCD | 是 |
顺序 | 数据映射,寄存器值字节顺序,和modbus poll软件格式一致,支持ABCD, CDAB,BADC,DCBA | 是 |
比例 | 数据转换,公式为 y = ax + b , a是比例 | 是 |
偏移 | 数据转换,公式为 y = ax + b , b是偏移 | 是 |
启用写入回复后对下行写入消息有个默认的回复格式;
从V93版本开始,回复内容变成输入一个模板函数,等效于模板模式第三个函数。 回复主题用于MQTT连接指定回复的主题
Modbus结果支持3种数据结构; 一般情况下,以及自适应对接内置的平台格式,请选择混合模式;想要简单的自定义上传格式请选择模板模式。
所有映射的键值全局唯一,结果将是一个只有一层的JSON Map
设置两条指令,指令一添加映射a,指令二添加映射b,输出结果:
{"a":10, "b":11}
下发格式和上传格式完全一致,网关会自动转换成modbus指令下发,如果启用了写入回复,会上报一条结果数据
若上传数据:
{"a":10, "b":11}
需要修改属性a的值,下发:
{"a":20}
网关会根据映射匹配到对应的指令,并转换成指令下发
所有映射的键值按设备唯一,结果将是一个有两层的JSON Array
设置两条指令,指令一添加映射a,指令二添加映射b,输出结果:
[{"c":3,"v":{"a":0},"i":1},{"c":3,"v":{"b":0},"i":2}]
c表示功能码,v表示映射列表,i表示设备id
下发格式和上传格式完全一致,网关会自动转换成modbus指令下发,如果启用了写入回复,会上报一条结果数据
若上传数据:
[{"c":3,"v":{"a":0},"i":1},{"c":3,"v":{"b":0},"i":2}]
需要修改属性a的值,下发:
[{"c":3,"v":{"a":10},"i":1}]
网关会根据映射匹配到设备ID为1
对应映射a
的指令,并转换成指令下发
混合模式下下发json,采集字段a和b
{"a":0, "b":0, "read":true}
设备模式下文档有点更新
数据结构切换到模板模式,需要设置上下行模板,也可以只设置一个。这里最后直接lua函数的方式实现模板,输入格式:
function(tm,td,ex) return tm end
,
function(sub,ex) return sub end
,
function(reply,ex) return reply end
新增的第三个回复模板是从V93以后才支持
输入的含义是三个函数
-- 输入参数分别是: tm=混合结构table,td=设备模式table,ex=用户参数
-- 返回一个table
function(tm,td,ex)
return tm
end
-- 输入参数分别是: 下行的数据table, 用户参数ex
-- 返回可以是4个值:
-- 第一个是table结果,数据table
-- 第二个是bool,代表是否主动读取,而不写入;
-- 第三个是串口命令,会将这个数据直接发到串口,用于某些自定义指令;
-- 第四个是msgid,用于标记此消息;
function(sub, ex)
return sub, read, nil, msgid
end
-- 输入参数分别是: reply=回复的table,ex=用户参数(如果有msgid会在ex里面,在加上其他的用户参数)
-- 如果是modbus, reply的属性{ suc = suc, prop = prop, errlist = errlist }
-- 如果是645,reply的属性{ suc = suc, add = add }
-- 返回一个table
function(reply,ex)
return reply
end
function(tm,td,ex)
tm.c = (tm.a or 0) + (tm.b or 0)
return table.merge(tm, ex)
end
若混合模式上传{"a":10, "b":11}
这里模板处理后上传{"a":10, "b":11, "c":21}
{"mgsid":1234, "ts":123456778, "param":{"a":1, "b":2}}
这里是需要写入参数a和b,但是数据在第二层嵌套里面,如果采用混合模式,这里将不能识别到写入指令;{"a":1, "b":2}
使用下行模板:function(tm,td,ex)
tm.c = (tm.a or 0) + (tm.b or 0)
return table.merge(tm, ex)
end,
function(sub)
return sub.param
end
结果为{"a":1, "b":2}
,设备将识别到写入指令注意默认的下行模板结果需要转换成混合模式的格式输入,也就是单层JSON
[{"c":3,"v":{"a":0},"i":1},{"c":3,"v":{"a":0},"i":2}]
简化版本可以是[{"v":{"a":0},"i":1},{"v":{"a":0},"i":2}] --id value为字符串或者数字均可
若服务器下发数据[{"1234":{"key1":"10","key2":"20"}},{"5678":{"key1":"30","key2":"40"}}] --这个格式不好取值,模板里面遍历两次
需要转换成 [{"v":{"key1":"10","key2":"20"},"i":"1234"},{"v":{"key1":"30","key2":"40"},"i":"5678"}]
使用下行模板:function(tm,td,ex)
tm.c = (tm.a or 0) + (tm.b or 0)
return table.merge(tm, ex)
end,
function (sub)
if #sub < 1 then return end
local devTemp = {}
for index, value in ipairs(sub) do
for key, val in pairs(value) do
table.insert(devTemp, {i=key, v=val})
end
end
return devTemp
end
若服务器下发数据{"mgsid":1234, "ts":123456778, "read":{"a":1, "b":2}}
这里是需要写入参数a和b,但是数据在第二层嵌套里面,如果采用混合模式,这里将不能识别到写入指令;
使用下行模板:
function(tm,td,ex)
tm.c = (tm.a or 0) + (tm.b or 0)
return table.merge(tm, ex)
end,
function(sub)
return sub.read, true
end
结果为{"a":1, "b":2}
,设备将触发一次对a和b的读取
使用模板
function(tm,td,ex)
return table.merge(tm, ex)
end,
function(sub)
return sub.data, sub.read
end
根据read字段决定是读取还是写入
{"msgid":123, "data":{"DO1":0}, "read":false}
function(tm,td,ex)
local template = {
version = "1.0",
type = "variant_data",
ts = os.time(),
params = {Va = 221.5},
time = 0
}
tm.c = (tm.a or 0) + (tm.b or 0)
template.params = table.merge(tm, ex)
return template
end
若混合模式上传{"a":10, "b":11}
这里模板处理后上传{"type":"variant_data","version":"1.0","ts":1656642045,"time":0,"params":{"c":33,"b":22,"a":11}}
function(tm,td,ex)
local template = {
msgtepe="test2",
data=td
}
return template
end
若混合模式上传{"IA":10, "IB":11......}
实际就只是取了设备模式的数据
这里模板处理后上传
{
"msgtepe":"test2",
"data":[
{
"c":3,
"v":{
"IA":0,
"IB":0,
"PS":0,
"UA":0,
"IC":0,
"UAB":0,
"EPP":0,
"PFS":0.99999,
"UBC":0,
"UC":0,
"UCA":0,
"UB":0,
"FRQ":0
},
"i":1
}
]
}
-- 在用户参数设置好键对
-- "1":"device1"
-- "2":"device2"
-- Modbus设置单独上传,根据id去用户参数里面找到id对应的设备名称,在上传
function(tm,td,ex)
if not td or #td < 1 then
return {}
end
local template = {
DeviceName = ex[tostring(td[1].i)] or "defaultname",
Timestamp = os.time()*1000,
DataMap = {Va = 221.5}
}
template.DataMap = tm
return template
end
若混合模式上传{"a":10, "b":11}
这里模板处理后上传
{
"DeviceName" : "device1",
"Timestamp": 1672281095000,
"DataMap" : {"a":10, "b":11}
}
-- 在用户参数设置好键对
-- "1":"device1"
-- "2":"device2"
-- Modbus设置单独上传,根据id去用户参数里面找到id对应的设备名称,在上传
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
table.insert(template,{["slaveNo"..id] = value.v})
end
return template
end
若设备模式上传[{"c":3,"v":{"IA": "12","Temp": 10.2},"i":1},{"c":3,"v":{"IA": "13", "Temp": 10},"i":2}]
这里模板处理后上传
{
"slaveNo1": {
"IA": 12,
"Temp": 10.2
},
"slaveNo2": {
"IA": 13,
"Temp": 10
}
}
需要的目标格式
{
"sensorDatas":
[
//数值型
{
"addTime":"2015-01-01 12:00:00",
"value":"10.0",
"flag":""
},
//定位型
{
"addTime":"2015-01-01 12:00:00",
"lat":39.9,
"lng":116.3,
"flag":""
},
//开关型
{
"addTime":"2015-01-01 12:00:00",
"switcher":"1",
"flag":""
},
//字符串
{
"addTime":"2015-01-01 12:00:00",
"str":"1",
"flag":""
}
]
}
-- 在用户参数设置好键对
-- "1":"device1"
-- "2":"device2"
-- Modbus设置单独上传,根据id去用户参数里面找到id对应的设备名称,在上传
function(tm,td,ex)
if not td or #td < 1 then return {} end
local template = {sensorDatas = {}}
local sensorDatas = {}
for key, value in pairs(tm) do
table.insert(sensorDatas, {
value=value,
flag=key
})
end
template.sensorDatas=sensorDatas
return template
end
这是流量计的数据, 由于结果是6字节加2字节拼接的,配置映射先采取两个long数据,经过计算拼接结果:
主站发送: 17 03 00 04 00 04 073E
地址 功能码 起始地址 寄存器数目 CRC校验码
从站响应:17 03 08 00000039412524E1 9D25
地址 功能码 字节数 标况累积量 校验码
变量数据为8字节二进制数表示的标准累积流量,高位在先,其中前6个字节为整数部分,后两个为小数部分,解包得数3752229.144Nm3/h。
附数据解包说明:
1) 整数部分十六进制数00 00 00 39 41 25等于十进制的3752229。 2) 小数部分十六进制数(24 E1)16 = (9441)10 / 65536= 0.14405(十进制小数)。 3) 结果为3752229.14,单位:Nm3/h
2) 小数部分十六进制数(24 E1)16 = (9441)10 / 65536= 0.14405(十进制小数)。
3) 结果为3752229.14,单位:Nm3/h
采集指令配置:
模板设置:
function(tm,td,ex)
if not tm.lj_h or not tm.lj_l then
return tm
end
-- 拼接结果
local lj = tm.lj_h * 65536 + tm.lj_l // 65536
lj = lj + (tm.lj_l % 65536) / 65536.0
tm.lj_h = nil
tm.lj_l = nil
tm.lj = lj
return table.merge(tm, ex)
end
直接拼接字符串
--目标结果:
--<south>
--<MsgType>1</MsgType>
--<PH>6</PH>
--<CO2>3.58</CO2>
--<TEMP>23.58</TEMP>
--</south>
function(tm,td,ex)
if not td or #td < 1 then return {} end
local template = {"<south>\n<MsgType>1</MsgType>\n"}
for index, value in pairs(tm) do
table.insert(template, "<")
table.insert(template, index)
table.insert(template, ">")
table.insert(template, value)
table.insert(template, "</")
table.insert(template, index)
table.insert(template, ">\n")
end
table.insert(template, "</south>")
return table.concat(template)
end
这里模板处理后上传
--混合模式结果:
{:PH":6,"CO2":3.58,"TEMP":23.58}
--输出:
<south>
<MsgType>1</MsgType>
<PH>6</PH>
<CO2>3.58</CO2>
<TEMP>23.58</TEMP>
</south>
采集指令配置:
其中lj_h代表整数,lj_l代表小数数据,最终数据要求输出lj_h或者一个新的键值,小数加整数
模板设置:
function(tm,td,ex)
--加法,结果也可以是新的键值
tm.lj_h = (tm.lj_h or 0) + (tm.lj_l or 0)
--删除lj_l
tm.lj_l = nil
return table.merge(tm, ex)
end
以下内容在app V89以上测试
混合模式下发数据写入DO1
字段, read
代表是读取还是写入
{"DO1":1, "msgid":12345,"read":false}
写入成功回复:
{
"suc" : true,
"prop" : {
"DO1" : 1
},
"msgid" : 12345
}
下行参数处理函数输入参数:下行的数据table(已经反序列化json);
返回可以是4个值
第一个是table结果
第二个是bool,代表是否主动读取,而不写入;
第三个是串口命令,会将这个数据直接发到串口,用于某些自定义指令;
第四个是msgid,用于标记此消息;
下发数据写入DO1
字段, read
代表是读取还是写入
{ "data":{ "DO1":1}, "msgid":12345, "read":false}
设置模板:
function(tm,td,ex)
return table.merge(tm, ex)
end,
function(sub)
local data = sub.data or {}
data.msgid = sub.msgid
return data, sub.read
end
设置模板, 方法2:
function(tm,td,ex)
return table.merge(tm, ex)
end,
function(sub)
return sub.data, sub.read, nil, sub.msgid
end
写入成功回复:
{
"suc" : true,
"prop" : {
"DO1" : 1
},
"msgid" : 12345
}
下发数据写入DO1
字段, read
代表是读取还是写入
{ "data":[{"c":1,"v":{"DO1":1},"i":1}], "msgid":12345, "read":false}
设置模板:
function(tm,td,ex)
return table.merge(td, ex)
end,
function(sub)
local data = sub.data or {}
data.msgid = sub.msgid
return data, sub.read
end
方法2:
function(tm,td,ex)
return table.merge(td, ex)
end,
function(sub)
return sub.data, sub.read, nil, sub.msgid
end
写入成功回复:
{
"prop" : {
"DO1" : 1
},
"msgid" : 12345,
"suc" : true
}
完整的测试用例需要使用sbl版本V93以上才能支持
{
"ts" : "2023-08-10 21:54:57",
"devid" : "861241057766154",
"data" : {
"100" : {
"AI2" : 0.386032,
"AI1" : 2.98898
},
"200" : {
"DO2" : 0,
"DO1" : 0
}
}
}
{
"ts" : "2023-08-10 22:08:15",
"devid" : "861241057766154",
"data" : {
"000001544992" : {
"DO2" : 10,
"DO1" : 220
},
"1234567" : {
"AI1" : 220,
"AI2" : 10
}
}
}
{
"data": [
{
"v": {
"DO1": 1
},
"i": "200"
}
],
"msgid": "12455",
"read": true
}
{
"msgid": "12455",
"data": {
"200": {
"DO1": 6718,
"DO2": 6721
}
},
"ts": "2023-08-12 11:54:20",
"devid": "861241057766154"
}
{
"data": [
{
"v": {
"AI1": 0
},
"i": "1234567"
}
],
"msgid": "12455",
"read": true
}
{
"devid": "861241057766154",
"data": {
"1234567": {
"AI2": 10,
"AI1": 220
}
},
"ts": "2023-08-11 22:31:54",
"msgid": "12455"
}
{
"data": [
{
"v": {
"DO1": 1
},
"i": "200"
}
],
"msgid": "12455",
"read": false
}
{
"msgid": "222222",
"data": {
"200": {
"DO1": 2,
"DO2": 7190
}
},
"ts": "2023-08-12 11:58:18",
"devid": "861241057766154"
}
{
"stat": true,
"msgid": "222222",
"prop": {
"DO1": 1
}
}
{
"data": [
{
"hz": "35444444333333334D334A8453433556",
"i": "1234567"
}
],
"msgid": "12455",
"read": false
}
{"stat":false,"msgid":"12455","msg":"null"}
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)
end
local res = { devid = imei, ts = os.date("%Y-%m-%d %H:%M:%S"), data = list, msgid=ex and ex.msgid }
return res
end
function (sub)
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 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)
end
local res = { devid = imei, ts = os.date("%Y-%m-%d %H:%M:%S"), data = list, msgid=ex and ex.msgid }
return res
end,
function (sub)
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