背景:现在主流的MobdusRTU从站设备云采集方案应该是走ModbusRTU转JSON然后通过MQTT/HTTP等方式上传,这个方案只需要一个物联网网关就能解决,而我本来在公司项目初期也是打算用这个方案的,代码逻辑都写好了,但结果半路杀出个程咬金,项目总监跟我说现场需要物联网网关支持POE供电,而我选的那个产品不支持POE供电所以只能PASS,但很不巧的是,其他厂家的产品虽支持POE供电,但又不支持ModbusRTU转JSON,只能支持透传,所以没有办法,只好采用通过ModbusRTU指令透传来实现采集的下下策。
主要代码:
RTU采集数据Entity:
package com.finder.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
* 第三方通风橱采集数据表
* </p>
*
* @author finder
* @since 2024-05-27
*/
@TableName("data_new_fumehood")
public class DataNewFumehood implements Serializable {
private static final long serialVersionUID = 1L;
public DataNewFumehood() {
}
public DataNewFumehood(Integer id, String deviceCode, Integer emergencyExhaustWindStatus, Integer windowHeight, BigDecimal windSpeed, BigDecimal temperature, Integer valveOpeningDegree, Integer openStatus, Integer closeStatus, Integer maxExhaustWindOpenStatus, Integer maxExhaustWindCloseStatus, Integer illuminationOpenStatus, Integer illuminationCloseStatus, Integer windowHeightOverlimitStatus, Integer windSpeedOverlimitStatus, Integer temperatureOverlimitStatus, Integer windowExistBarrierStatus, LocalDateTime acquisitionTime) {
this.id = id;
this.deviceCode = deviceCode;
this.emergencyExhaustWindStatus = emergencyExhaustWindStatus;
this.windowHeight = windowHeight;
this.windSpeed = windSpeed;
this.temperature = temperature;
this.valveOpeningDegree = valveOpeningDegree;
this.openStatus = openStatus;
this.closeStatus = closeStatus;
this.maxExhaustWindOpenStatus = maxExhaustWindOpenStatus;
this.maxExhaustWindCloseStatus = maxExhaustWindCloseStatus;
this.illuminationOpenStatus = illuminationOpenStatus;
this.illuminationCloseStatus = illuminationCloseStatus;
this.windowHeightOverlimitStatus = windowHeightOverlimitStatus;
this.windSpeedOverlimitStatus = windSpeedOverlimitStatus;
this.temperatureOverlimitStatus = temperatureOverlimitStatus;
this.windowExistBarrierStatus = windowExistBarrierStatus;
this.acquisitionTime = acquisitionTime;
}
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 设备编号
*/
private String deviceCode;
/**
* 紧急排风状态
*/
private Integer emergencyExhaustWindStatus;
/**
* 视窗1高度
*/
private Integer windowHeight;
/**
* 当前实际面风速(m/s)
*/
private BigDecimal windSpeed;
/**
* 温度(℃)
*/
private BigDecimal temperature;
/**
* 阀开度(%)
*/
private Integer valveOpeningDegree;
/**
* 开机状态
*/
private Integer openStatus;
/**
* 关机状态
*/
private Integer closeStatus;
/**
* 最大排风开启状态
*/
private Integer maxExhaustWindOpenStatus;
/**
* 最大排风关闭状态
*/
private Integer maxExhaustWindCloseStatus;
/**
* 照明开启状态
*/
private Integer illuminationOpenStatus;
/**
* 照明关闭状态
*/
private Integer illuminationCloseStatus;
/**
* 视窗1高度超限状态(0正常/1超限)
*/
private Integer windowHeightOverlimitStatus;
/**
* 面风速超限状态(0正常/1超限)
*/
private Integer windSpeedOverlimitStatus;
/**
* 温度超限状态(0正常/1超限)
*/
private Integer temperatureOverlimitStatus;
/**
* 防夹检测开关状态(0正常/1有障碍物)
*/
private Integer windowExistBarrierStatus;
/**
* 返回报文(十六进制显示的字符串)
*/
private String hexStr;
/**
* 采集时间
*/
private LocalDateTime acquisitionTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
public Integer getEmergencyExhaustWindStatus() {
return emergencyExhaustWindStatus;
}
public void setEmergencyExhaustWindStatus(Integer emergencyExhaustWindStatus) {
this.emergencyExhaustWindStatus = emergencyExhaustWindStatus;
}
public Integer getWindowHeight() {
return windowHeight;
}
public void setWindowHeight(Integer windowHeight) {
this.windowHeight = windowHeight;
}
public BigDecimal getWindSpeed() {
return windSpeed;
}
public void setWindSpeed(BigDecimal windSpeed) {
this.windSpeed = windSpeed;
}
public BigDecimal getTemperature() {
return temperature;
}
public void setTemperature(BigDecimal temperature) {
this.temperature = temperature;
}
public Integer getValveOpeningDegree() {
return valveOpeningDegree;
}
public void setValveOpeningDegree(Integer valveOpeningDegree) {
this.valveOpeningDegree = valveOpeningDegree;
}
public Integer getOpenStatus() {
return openStatus;
}
public void setOpenStatus(Integer openStatus) {
this.openStatus = openStatus;
}
public Integer getCloseStatus() {
return closeStatus;
}
public void setCloseStatus(Integer closeStatus) {
this.closeStatus = closeStatus;
}
public Integer getMaxExhaustWindOpenStatus() {
return maxExhaustWindOpenStatus;
}
public void setMaxExhaustWindOpenStatus(Integer maxExhaustWindOpenStatus) {
this.maxExhaustWindOpenStatus = maxExhaustWindOpenStatus;
}
public Integer getMaxExhaustWindCloseStatus() {
return maxExhaustWindCloseStatus;
}
public void setMaxExhaustWindCloseStatus(Integer maxExhaustWindCloseStatus) {
this.maxExhaustWindCloseStatus = maxExhaustWindCloseStatus;
}
public Integer getIlluminationOpenStatus() {
return illuminationOpenStatus;
}
public void setIlluminationOpenStatus(Integer illuminationOpenStatus) {
this.illuminationOpenStatus = illuminationOpenStatus;
}
public Integer getIlluminationCloseStatus() {
return illuminationCloseStatus;
}
public void setIlluminationCloseStatus(Integer illuminationCloseStatus) {
this.illuminationCloseStatus = illuminationCloseStatus;
}
public Integer getWindowHeightOverlimitStatus() {
return windowHeightOverlimitStatus;
}
public void setWindowHeightOverlimitStatus(Integer windowHeightOverlimitStatus) {
this.windowHeightOverlimitStatus = windowHeightOverlimitStatus;
}
public Integer getWindSpeedOverlimitStatus() {
return windSpeedOverlimitStatus;
}
public void setWindSpeedOverlimitStatus(Integer windSpeedOverlimitStatus) {
this.windSpeedOverlimitStatus = windSpeedOverlimitStatus;
}
public Integer getTemperatureOverlimitStatus() {
return temperatureOverlimitStatus;
}
public void setTemperatureOverlimitStatus(Integer temperatureOverlimitStatus) {
this.temperatureOverlimitStatus = temperatureOverlimitStatus;
}
public Integer getWindowExistBarrierStatus() {
return windowExistBarrierStatus;
}
public void setWindowExistBarrierStatus(Integer windowExistBarrierStatus) {
this.windowExistBarrierStatus = windowExistBarrierStatus;
}
public String getHexStr() {
return hexStr;
}
public void setHexStr(String hexStr) {
this.hexStr = hexStr;
}
public LocalDateTime getAcquisitionTime() {
return acquisitionTime;
}
public void setAcquisitionTime(LocalDateTime acquisitionTime) {
this.acquisitionTime = acquisitionTime;
}
@Override
public String toString() {
return "DataNewFumehood{" +
"id=" + id +
", deviceCode='" + deviceCode + '\'' +
", emergencyExhaustWindStatus=" + emergencyExhaustWindStatus +
", windowHeight=" + windowHeight +
", windSpeed=" + windSpeed +
", temperature=" + temperature +
", valveOpeningDegree=" + valveOpeningDegree +
", openStatus=" + openStatus +
", closeStatus=" + closeStatus +
", maxExhaustWindOpenStatus=" + maxExhaustWindOpenStatus +
", maxExhaustWindCloseStatus=" + maxExhaustWindCloseStatus +
", illuminationOpenStatus=" + illuminationOpenStatus +
", illuminationCloseStatus=" + illuminationCloseStatus +
", windowHeightOverlimitStatus=" + windowHeightOverlimitStatus +
", windSpeedOverlimitStatus=" + windSpeedOverlimitStatus +
", temperatureOverlimitStatus=" + temperatureOverlimitStatus +
", windowExistBarrierStatus=" + windowExistBarrierStatus +
", acquisitionTime=" + acquisitionTime +
'}';
}
}
RTU寄存器地址类:
package com.finder.system.rtuRegisterAddress;
public class NewFumeHood {
/**
* 运行状态寄存器1
*/
public static int runningStatusRegister1=1;
/**
* 紧急排风状态
*/
public static String emergencyExhaustWindStatus="1.1";
/**
* 视窗1高度
*/
public static int windowHeight=2;
/**
* 当前实际面风速(m/s)
*/
public static int windSpeed=3;
/**
* 温度(℃)
*/
public static int temperature=30;
/**
* 阀开度(%)
*/
public static int valveOpeningDegree=39;
/**
* 运行控制寄存器1
*/
public static int runningControlRegister=5;
/**
* 开机状态
*/
public static String openStatus="5.0";
/**
* 关机状态
*/
public static String closeStatus="5.1";
/**
* 最大排风开启状态
*/
public static String maxExhaustWindOpenStatus="5.2";
/**
* 最大排风关闭状态
*/
public static String maxExhaustWindCloseStatus="5.3";
/**
* 照明开启状态
*/
public static String illuminationOpenStatus="5.4";
/**
* 照明关闭状态
*/
public static String illuminationCloseStatus="5.5";
/**
* 运行状态寄存器2
*/
public static int runningStatusRegister2=59;
/**
* 视窗1高度超限状态(0正常/1超限)
*/
public static String windowHeightOverlimitStatus="59.0";
/**
* 面风速超限状态(0正常/1超限)
*/
public static String windSpeedOverlimitStatus="59.1";
/**
* 温度超限状态(0正常/1超限)
*/
public static String temperatureOverlimitStatus="59.2";
/**
* 防夹检测开关状态(0正常/1有障碍物)
*/
public static String windowExistBarrierStatus="59.9";
}
字节数组转化工具类:
package com.finder.system.utils;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
public class BytesUtils {
/**
* 字节数组转整形(2字节)
* 大端序列
*/
public static Integer bytesToInt16(byte[] bytes,boolean isSigned){
short result=ByteBuffer.wrap(bytes).getShort();
return isSigned?result:result & 0xFFFF;
}
/**
* 字节数组转整形(4字节)
* 大端序列
*/
public static Long bytesToInt32(byte[] bytes,boolean isSigned){
int result=ByteBuffer.wrap(bytes).getInt();
return isSigned?result:result & 0xFFFFFFFFL;
}
/**
* 字节数组转单精度浮点(4字节)
* 大端序列
*/
public static BigDecimal bytesToFloat(byte[] bytes){
return BigDecimal.valueOf(ByteBuffer.wrap(bytes).getFloat());
}
/**
* 字节数组转双精度浮点(8字节)
* 大端序列
*/
public static BigDecimal bytesToDouble(byte[] bytes){
return BigDecimal.valueOf(ByteBuffer.wrap(bytes).getDouble());
}
/**
* 获取字节数组中特定位的值
*/
public static Integer getBit(byte[] bytes,int byteIndex, int bitIndex){
if (byteIndex < 0 || byteIndex >= bytes.length) {
throw new IllegalArgumentException("Byte index out of range");
}
if (bitIndex < 0 || bitIndex > 7) {
throw new IllegalArgumentException("Bit index must be between 0 and 7");
}
byte targetByte = bytes[byteIndex];
return (targetByte >> bitIndex) & 1;
}
/**
* 设置字节数组中特定位的值
*/
public static byte[] setBit(byte[] bytes,int byteIndex, int bitIndex, int newValue){
if (byteIndex < 0 || byteIndex >= bytes.length) {
throw new IllegalArgumentException("Byte index out of range");
}
if (bitIndex < 0 || bitIndex > 7) {
throw new IllegalArgumentException("Bit index must be between 0 and 7");
}
if (newValue != 0 && newValue != 1) {
throw new IllegalArgumentException("New value must be 0 or 1");
}
byte targetByte = bytes[byteIndex];
targetByte = (byte) (targetByte & ~(1 << bitIndex)); // 清除目标位
targetByte = (byte) (targetByte | (newValue << bitIndex)); // 设置目标位
byte[] modifiedBytes = bytes.clone(); // 复制字节数组
modifiedBytes[byteIndex] = targetByte; // 更新目标字节
return modifiedBytes;
}
}
定时任务配置类:
package com.finder.system.schedule;
import com.finder.system.mqtt.MqttSendClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import java.time.LocalDateTime;
@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
private static final Logger logger = LoggerFactory.getLogger(SchedulingConfiguration.class);
@Autowired
MqttSendClient mqttSendClient;
/**
* 定时任务使用的线程池
*
* @return
*/
@Bean(destroyMethod = "shutdown", name = "taskScheduler")
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler() {
//默认情况下 ScheduledThreadPoolExecutor 将等待所有延迟的计划任务完成执行,即使计划任务当时没有运行也会等待。
private static final long serialVersionUID = -1L;
@Override
public void destroy() {
// 设置只等待当前正在运行的计划任务完成执行
this.getScheduledThreadPoolExecutor().setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
super.destroy();
}
};
scheduler.setPoolSize(20);
scheduler.setThreadNamePrefix("task-");
scheduler.setAwaitTerminationSeconds(600);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = taskScheduler();
scheduledTaskRegistrar.setTaskScheduler(taskScheduler);
//样板间通风橱数据获取
scheduledTaskRegistrar.addTriggerTask(
()->{
logger.debug("sendRTUReadCommand >>>>>>>>>>> " + LocalDateTime.now().toLocalTime());
byte[] commandBytes=new byte[]{0x01,0x03,0x00,0x00,0x00,0x3C,0x45, (byte) 0xDB};
sendRTUReadCommand("287481C48A0B",commandBytes);
},
triggerContext -> {
String sendRTUReadCommandCron = "0 */1 * * * ?";//默认每分钟执行一次
return new CronTrigger(sendRTUReadCommandCron).nextExecutionTime(triggerContext);
}
);
}
/**
* 定时发送指令到IOT网关再传给485RTU设备
* 终端收到指令上传数据,仅适合modbusRTU协议的传感器
*/
public void sendRTUReadCommand(String deviceCode, byte[] bytes) {
logger.debug("定时发送指令到无线传输终端上:" + LocalDateTime.now().toLocalTime() + "=====使用的线程 : " + Thread.currentThread().getName());
mqttSendClient.bytesPublish(0, false, "/rtu/downlink/" + deviceCode,bytes);
}
}
RTU返回报文解析及数据持久化:
/**
* RTU返回报文解析及数据持久化
*/
@Override
public void dealRTUData(byte[] payload) {
logger.info("收到RTU返回报文:"+showBytes(payload,16));
if(payload.length==125) {
//截取modbusRTU返回报文中采集的60个寄存器的数据
byte[] dataBytes=Arrays.copyOfRange(payload,3,123);
String hexStr = showBytes(payload,16);
DataNewFumehood dataNewFumehood = new DataNewFumehood();
//获取运行状态寄存器1的值
byte[] runningStatusRegister1Bytes=subBytes(dataBytes,NewFumeHood.runningStatusRegister1*2,2);
//logger.info("运行状态寄存器1的值为:"+ showBytes(runningStatusRegister1Bytes,2));
//紧急排风状态
dataNewFumehood.setEmergencyExhaustWindStatus(getBitValue(NewFumeHood.emergencyExhaustWindStatus,runningStatusRegister1Bytes));
//视窗1高度
byte[] windowHeightBytes=subBytes(dataBytes,NewFumeHood.windowHeight*2,2);
dataNewFumehood.setWindowHeight(BytesUtils.bytesToInt16(windowHeightBytes,true));
//当前实际面风速(m/s)
byte[] windSpeedBytes=subBytes(dataBytes,NewFumeHood.windSpeed*2,2);
int windSpeedInt=BytesUtils.bytesToInt16(windSpeedBytes,false);
dataNewFumehood.setWindSpeed(BigDecimal.valueOf((float)windSpeedInt/1000));
//温度(℃)
byte[] temperatureBytes=subBytes(dataBytes,NewFumeHood.temperature*2,2);
int temperatureInt=BytesUtils.bytesToInt16(temperatureBytes,true);
dataNewFumehood.setTemperature(BigDecimal.valueOf((float)temperatureInt/10));
//阀开度(%)
byte[] valveOpeningDegreeBytes=subBytes(dataBytes,NewFumeHood.valveOpeningDegree*2,2);
dataNewFumehood.setValveOpeningDegree(BytesUtils.bytesToInt16(valveOpeningDegreeBytes,false));
//获取运行控制寄存器1的值
byte[] runningControlRegisterBytes=subBytes(dataBytes,NewFumeHood.runningControlRegister*2,2);
//logger.info("运行控制寄存器1的值为:"+showBytes(runningControlRegisterBytes,2));
//开机状态
dataNewFumehood.setOpenStatus(getBitValue(NewFumeHood.openStatus,runningControlRegisterBytes));
//关机状态
dataNewFumehood.setCloseStatus(getBitValue(NewFumeHood.closeStatus,runningControlRegisterBytes));
//最大排风开启状态
dataNewFumehood.setMaxExhaustWindOpenStatus(getBitValue(NewFumeHood.maxExhaustWindOpenStatus,runningControlRegisterBytes));
//最大排风关闭状态
dataNewFumehood.setMaxExhaustWindCloseStatus(getBitValue(NewFumeHood.maxExhaustWindCloseStatus,runningControlRegisterBytes));
//照明开启状态
dataNewFumehood.setIlluminationOpenStatus(getBitValue(NewFumeHood.illuminationOpenStatus,runningControlRegisterBytes));
//照明关闭状态
dataNewFumehood.setIlluminationCloseStatus(getBitValue(NewFumeHood.illuminationCloseStatus,runningControlRegisterBytes));
//获取运行控制寄存器2的值
byte[] runningStatusRegister2Bytes=subBytes(dataBytes,NewFumeHood.runningStatusRegister2*2,2);
//logger.info("运行状态寄存器2的值为:"+ showBytes(runningStatusRegister2Bytes,2));
//视窗1高度超限状态(0正常/1超限)
dataNewFumehood.setWindowHeightOverlimitStatus(getBitValue(NewFumeHood.windowHeightOverlimitStatus,runningStatusRegister2Bytes));
//面风速超限状态(0正常/1超限)
dataNewFumehood.setWindSpeedOverlimitStatus(getBitValue(NewFumeHood.windSpeedOverlimitStatus,runningStatusRegister2Bytes));
//温度超限状态(0正常/1超限)
dataNewFumehood.setTemperatureOverlimitStatus(getBitValue(NewFumeHood.temperatureOverlimitStatus,runningStatusRegister2Bytes));
//防夹检测开关状态(0正常/1有障碍物)
dataNewFumehood.setWindowExistBarrierStatus(getBitValue(NewFumeHood.windowExistBarrierStatus,runningStatusRegister2Bytes));
dataNewFumehood.setHexStr(hexStr);
dataNewFumehoodMapper.insert(dataNewFumehood);
}
}
/**
* 截取字节数组
*/
private byte[] subBytes(byte[] dataBytes,int startByteIndex,int length){
byte[] subBytes=new byte[length];
System.arraycopy(dataBytes,startByteIndex,subBytes,0,length);
return subBytes;
}
/**
* 获取寄存器的位数据
*/
private int getBitValue(String registerAddress,byte[] registerBytes){
int bitIndex=Integer.parseInt(registerAddress.split("\\.")[1]);
return BytesUtils.getBit(registerBytes,1-bitIndex/8,bitIndex%8);
}
/**
* 将字节数组按指定进制进行字符串输出
*/
private String showBytes(byte[] bytes,int radix) {
StringBuilder sb = new StringBuilder();
switch (radix) {
case 16:
for (byte b : bytes) {
// 将每个字节转换为两位的十六进制表示
sb.append(String.format("%02X", b));
}
break;
case 2:
for (byte b : bytes) {
// 将每个字节转换为八位的二进制表示
sb.append(String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'));
}
}
return sb.toString();
}

2149

被折叠的 条评论
为什么被折叠?



