智慧农业平台项目中服务器接收到设备发来的传感器数据后的处理过程说明

199 阅读8分钟

在基于JeePlus框架开发的智慧农业平台中,设备发来的传感器数据是遵循MODBUS-RTU协议的原始报文,本文是后端对该报文处理过程的说明。

核心流程在com.jeeplus.sensor.SensorProcessThread.javarun()

输入流接收到数据

将第一组二进制数据被读入到缓存数组byte[] buf中(对于每一个设备,第一组报文总是一样的,可能包含了标识信息,但由于不清楚标识信息是如何组织的,这里暂时以下面的name计算结果作为身份信息)

String name = Math.abs(new String(buf).hashCode() % 11953) +""作为设备的id,将当前时间作为设备的更新时间,将在线状态设置为1,添加到数据库表sensor_info中.(不理解为什么这个数据库表叫传感器信息,叫“设备来访”好一点)

如在2023-05-20 09:22:29收到了一个id为8600的设备发送的数据

image.png

表sensor_info

image.png

数据库表device_manage中存储了事先人为添加的设备,其中识别码sn设置为该设备发包哈希得到的name(即Math.abs(new String(buf).hashCode() % 11953) +"")

根据设备的idname在表device_manage中查找该设备deviceId以及设备对应的变量模板templateId,若该设备的idname不与表中sn对应,deviceIdtemplateId为空,则表明该设备未添加到列表中,线程退出

如上图id为1979的设备就未添加,而id为8600的设备提人为添加了(sn)

image.png

表device_manage

image.png

若接收到的报文来源于事先添加的设备

成功获得了变量模板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

image.png

表device_var_template

image.png

成功获得了设备deviceId,则在数据库表device_sensor中查询该设备deviceId对应的所有传感器列表,变量sensorsMap.put(sensor.getSensorName(), sensor.getId()),如设备id为1589565729570799617的sensorsMap变量中有一组{空气湿度(140cm):1589634651323002882}

如果该设备的传感器列表中未完全包含上面的变量模板,还会在表device_sensor中添加该设备缺失的传感器变量信息

image.png

表device_sensor

image.png

二进制数据处理为可视变量信息

根据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,将deviceSensorDatavalue赋值为String.valueOf(ushortValue)

  • "short":int shortValue = getUnsignedShort(new byte[]{buf[addr], buf[addr + 1]});addr += 2;将byte数组的下两项转为短整型并存到shortValue,将deviceSensorDatavalue赋值为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,将deviceSensorDatavalue赋值为String.valueOf(ulongABCD)

  • default:将deviceSensorDatavalue赋值为"0"

4.将deviceSensorDatasensorId赋值为当前变量模板中变量名称所对应的传感器id,将deviceSensorDatadatetime赋值为当前时间,将该deviceSensorData添加到deviceSensorDataList数组中

deviceSensorDataList数组的每一项插入到数据表device_sensor_data

下表显示了sensorId为1564565557904351234的传感器所测量的11点左右的值,该传感器属于设备1560105319398592513,测量的是光照强度(140cm)值

image.png

表device_sensor_data

image.png

例:现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;

image.png