在基于JeePlus框架开发的智慧农业平台中,设备发来的传感器数据是遵循MODBUS-RTU协议的原始报文,本文是后端对该报文处理过程的说明。
核心流程在com.jeeplus.sensor.SensorProcessThread.java的run()中
输入流接收到数据
将第一组二进制数据被读入到缓存数组byte[] buf中(对于每一个设备,第一组报文总是一样的,可能包含了标识信息,但由于不清楚标识信息是如何组织的,这里暂时以下面的name计算结果作为身份信息)
将String name = Math.abs(new String(buf).hashCode() % 11953) +""作为设备的id,将当前时间作为设备的更新时间,将在线状态设置为1,添加到数据库表sensor_info中.(不理解为什么这个数据库表叫传感器信息,叫“设备来访”好一点)
如在2023-05-20 09:22:29收到了一个id为8600的设备发送的数据
表sensor_info
数据库表device_manage中存储了事先人为添加的设备,其中识别码sn设置为该设备发包哈希得到的name(即Math.abs(new String(buf).hashCode() % 11953) +"")
根据设备的idname在表device_manage中查找该设备deviceId以及设备对应的变量模板templateId,若该设备的idname不与表中sn对应,deviceId或templateId为空,则表明该设备未添加到列表中,线程退出
如上图id为1979的设备就未添加,而id为8600的设备提人为添加了(sn)
表device_manage
若接收到的报文来源于事先添加的设备
成功获得了变量模板templateId,则在数据库表device_var_template中查询该模板templateId对应的变量细则,返回一个数组varTemplateDetails,其中每一项包含了一行变量名称,变量单位,数据类型等信息
如sn为8600的设备templateId为1566113790942146561,varTemplateDetails[0]应该包含该数据库第一行的所有信息1566118650068545537|1566113790942146561|空气检测|1|1|空气湿度(140cm)|%RH|variable10001|40001|ushort|1|1|2|2|x/10.0||1|2022-09-03|17:39:18|1|2022-09-03 17:39:18|0
表device_var_template
成功获得了设备deviceId,则在数据库表device_sensor中查询该设备deviceId对应的所有传感器列表,变量sensorsMap.put(sensor.getSensorName(), sensor.getId()),如设备id为1589565729570799617的sensorsMap变量中有一组{空气湿度(140cm):1589634651323002882}
如果该设备的传感器列表中未完全包含上面的变量模板,还会在表device_sensor中添加该设备缺失的传感器变量信息
表device_sensor
二进制数据处理为可视变量信息
根据modbus-rtu协议,原始报文第一个字节为从机地址(从机地址目前有两个,“1”代表从机名称为“空气检测”,“2”代表从机名称为“土壤墒情”), 第三个字节开始才为数据内容(int addr = 3;),最后两个字节为校验和
还需对数组varTemplateDetails重新排序,数组中变量模板的顺序应该与二进制数据一一对应,使用每一个变量模板中.getRegister()寄存器排序,寄存器在前的变量,其数据在二进制数据中也靠前
varTemplateDetails.sort((t1, t2) -> {
int r1 = Integer.parseInt(t1.getRegister());
int r2 = Integer.parseInt(t2.getRegister());
if (r1 > r2) return 1;
else if (r1 == r2) return 0;
else return -1;
});
循环:对于数组varTemplateDetails的每一项:
1.判断该项变量模板是否是数据所对应的从机名称.如该变量模板的slaveAddr是2(表明该项变量属于土壤墒情,如含钾量等),但报文第一个字节却为01(表明这是一组关于空气检测的数据),则跳过该项
2.获得当前变量模板的数据类型String dataFormat = curVarTemplateDetail.getDataFormat();准备一个对象deviceSensorData
//package com.jeeplus.sensordatadetail.domain;
public class DeviceSensorData extends BaseEntity {
private String sensorId;
private String datetime;
private String value;
}
3.依据dataFormat的值:
"ushort":int ushortValue = getUnsignedShort(new byte[]{buf[addr], buf[addr + 1]});addr += 2;将byte数组的下两项转为无符号短整型并存到ushortValue,将deviceSensorData的value赋值为String.valueOf(ushortValue)
"short":int shortValue = getUnsignedShort(new byte[]{buf[addr], buf[addr + 1]});addr += 2;将byte数组的下两项转为短整型并存到shortValue,将deviceSensorData的value赋值为String.valueOf(shortValue)
"ulong-ABCD":long ulongABCD = getUnsignedInt(new byte[]{buf[addr], buf[addr + 1], buf[addr + 2], buf[addr + 3]});addr += 4;将byte数组的下四项转为无符号长整型并存到ulongABCD,将deviceSensorData的value赋值为String.valueOf(ulongABCD)
default:将deviceSensorData的value赋值为"0"
4.将deviceSensorData的sensorId赋值为当前变量模板中变量名称所对应的传感器id,将deviceSensorData的datetime赋值为当前时间,将该deviceSensorData添加到deviceSensorDataList数组中
将deviceSensorDataList数组的每一项插入到数据表device_sensor_data中
下表显示了sensorId为1564565557904351234的传感器所测量的11点左右的值,该传感器属于设备1560105319398592513,测量的是光照强度(140cm)值
表device_sensor_data
例:现buf里是报文
buf[0] buf[1] buf[2] buf[3-12] buf[13-14]
从机地址 功能码 数据字节个数 数据 校验和
|01| |03| |0A| |01 69|00 FB|01 E1|00 00 1E 0F| |XX XX|
从机地址“01”代表从机名称为“空气检测”,这是一条包含了“空气检测”的数据的报文
功能码是MODBUS-RTU数据帧格式所要求的,“03”表示了对保持寄存器进行读功能,详情见MODBUS-RTU数据帧格式、报文实例_modbus协议报文-CSDN博客
数据字节个数“0A”表示接续的数据字段长度为10个字节
校验和为整条报文的循环冗余校验
排好序的数组varTemplateDetails内容大致如下
[0] 空气检测 1 空气湿度(140cm) %RH 40001 ushort x/10.0
[1] 土壤墒情 2 含水率(北-10cm) % 40001 ushort x/10.0
[2] 空气检测 1 空气温度(140cm) ℃ 40002 ushort x/10.0
[3] 土壤墒情 2 土壤温度(北-10cm) ℃ 40002 short x/10.0
[4] 空气检测 1 CO2浓度(140cm) ppm 40003 ushort
[5] 土壤墒情 2 电导率(北-10cm) us/cm 40003 ushort
[6] 空气检测 1 光照强度(140cm) Lux 40004 ulong-ABCD
[7] 土壤墒情 2 ph值(北-10cm) PH 40004 ushort x/10.0
[8] 土壤墒情 2 含氮量(北-10cm) mg/kg 40005 ushort
[9] 土壤墒情 2 含磷量(北-10cm) mg/kg 40006 ushort
[10] 土壤墒情 2 含钾量(北-10cm) mg/kg 40007 ushort
进入循环,第一项为从机地址为1的空气检测的空气湿度(140cm),类型为ushort,即读取两个字节的数据|01 69|,数据还要除以10,即36.1%RH;第二项为从机地址为2的土壤墒情的含水率(北-10cm),从机地址为1,跳过;第三项为从机地址为1的空气检测的空气温度(140cm),类型为ushort,即读取两个字节的数据|00 FB|,即为251,数据还要除以10,即25.1℃;以此类推;第七项为从机地址为1的空气检测的光照强度(140cm),类型为ulong-ABCD,即读取四个字节的数据|00 00 1E 0F|,即7695Lux;后面的都跳过;循环结束
该报文被解读为
"从机":"空气检测",
"空气湿度(140cm)":"36.1%RH",
"空气温度(140cm)":"25.1℃",
"CO2浓度(140cm)":"481ppm",
"光照强度(140cm)":"7695Lux"
随后每一个数据和该变量所对应的传感器id作为一行存入表device_sensor_data中以待查询
同理:报文
buf[0] buf[1] buf[2] buf[3-16] buf[17-18]
从机地址 功能码 数据字节个数 数据 校验和
|02| |03| |0E| |01 9C|00 9D|02 56|00 27|4B 38|3A BA|1F 95| |XX XX|
被解读为
"从机":"土壤墒情",
"含水率(北-10cm)":"41.2%",
"土壤温度(北-10cm)":"15.7℃",
"电导率(北-10cm)":"598us/cm"
"ph值(北-10cm)":"3.9PH",
"含氮量(北-10cm)":"19256mg/kg",
"含磷量(北-10cm)":"15034mg/kg",
"含钾量(北-10cm)":"8085mg/kg",
若想在前端查看某设备的最新数据,则在数据大屏的数据源中设置如下SQL语句
WITH latest_data AS (
SELECT
sensor_id,
MAX(datetime) AS max_datetime
FROM
device_sensor_data
GROUP BY
sensor_id
)
SELECT
ds.id,
ds.device_id,
ds.sensor_name,
ds.unit,
dsd.datetime,
dsd.value
FROM
device_sensor AS ds
JOIN
latest_data AS ld ON ds.id = ld.sensor_id
JOIN
device_sensor_data AS dsd ON ld.sensor_id = dsd.sensor_id AND ld.max_datetime = dsd.datetime;