简介
本文介绍如何使用java程序简单读取modbus slave端程序。
相关代码、软件资源,可参考附录部分。
概念
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。目前在和各大用电量采集厂商对接时,也几乎都用此协议进行沟通,因此对于做工业领域相关的技术人员,有必要对此协议进行一定的了解。
slave/master端 对于熟悉C/S、B/S模式的编程人员来说,modbus协议也同样分为两者,一个是slave端,一个是master端,我们一般编写采集程序可以认为是master端,而持续提供程序的端则称为slave端,常见的slave端可以使用modbus poll这类程序模拟,安装程序会放在文末附录最后。
modbus poll 常见的slave端模拟程序,可通过极简的配置模拟数据生成,方便数据采集人员本地调试。
modbus-master-tcp本文所采用的第三方数据读取包。
实例程序
1、编写读取程序(master端)
前面提到,读取程序一般充当master端,因此我们先编写主程序,用于采集指定slave端的数据。
这里slave端一般是我们本地的modbus poll工具,下一章节介绍此工具的简单使用。
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.digitalpetri.modbus</groupId>
<artifactId>modbus-master-tcp</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
主读取程序
package com.example.modbustest.md;
import com.digitalpetri.modbus.master.ModbusTcpMaster;
import com.digitalpetri.modbus.master.ModbusTcpMasterConfig;
import com.digitalpetri.modbus.requests.ReadCoilsRequest;
import com.digitalpetri.modbus.requests.ReadDiscreteInputsRequest;
import com.digitalpetri.modbus.requests.ReadHoldingRegistersRequest;
import com.digitalpetri.modbus.requests.ReadInputRegistersRequest;
import com.digitalpetri.modbus.responses.ReadCoilsResponse;
import com.digitalpetri.modbus.responses.ReadDiscreteInputsResponse;
import com.digitalpetri.modbus.responses.ReadHoldingRegistersResponse;
import com.digitalpetri.modbus.responses.ReadInputRegistersResponse;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* Desc:
* ModBus组件
*
* @author JoyBlack
* @email 1016037677@qq.com
* @date 2023/1/6 下午 3:27
*/
public class ModBus {
private ModbusTcpMaster master;
private static final Logger logger = LoggerFactory.getLogger(ModBus.class);
public ModBus(String address, int port) {
ModbusTcpMasterConfig config = new ModbusTcpMasterConfig.Builder(address)
.setPort(port)
.setTimeout(Duration.ofSeconds(20))
.build();
master = new ModbusTcpMaster(config);
}
/**
* Desc:
* 释放资源
*
* @author JoyBlack
* @email 1016037677@qq.com
* @date 2023/1/5 上午 10:04
*/
public void release() {
if (master != null) {
master.disconnect();
}
// Modbus.releaseSharedResources();
}
/**
* Desc:
* 读取HoldingRegister数据(address: 寄存器地址; quantity: 寄存器数量; unitId: id)
*
* @author JoyBlack
* @email 1016037677@qq.com
* @date 2023/1/5 上午 10:04
*/
public Number readHoldingRegister(int address, int quantity, int unitId) throws Exception {
Number result = null;
CompletableFuture<ReadHoldingRegistersResponse> future = master.sendRequest(new ReadHoldingRegistersRequest(address, quantity), unitId);
ReadHoldingRegistersResponse response = future.get(8, TimeUnit.SECONDS);
if (response != null) {
ByteBuf registers = response.getRegisters();
result = registers.readFloat();
ReferenceCountUtil.release(response);
}
return result;
}
/**
* Desc:
* 读取InputRegisters模拟量数据(address: 寄存器地址; quantity: 寄存器数量; unitId: id)
*
* @author JoyBlack
* @email 1016037677@qq.com
* @date 2023/1/5 上午 10:04
*/
public Number readInputRegister(int address, int quantity, int unitId) throws Exception {
Number result = null;
CompletableFuture<ReadInputRegistersResponse> future = master
.sendRequest(new ReadInputRegistersRequest(address, quantity), unitId);
// 工具类做的同步返回.实际使用推荐结合业务进行异步处理
ReadInputRegistersResponse readInputRegistersResponse = future.get();
if (readInputRegistersResponse != null) {
ByteBuf buf = readInputRegistersResponse.getRegisters();
result = buf.readFloat();
ReferenceCountUtil.release(readInputRegistersResponse);
}
return result;
}
/**
* Desc:
* 读取Coils开关量
*
* @author JoyBlack
* @email 1016037677@qq.com
* @date 2023/1/5 上午 10:04
*/
public Boolean readCoils(int address, int quantity, int unitId)
throws Exception {
Boolean result = null;
CompletableFuture<ReadCoilsResponse> future = master.sendRequest(new ReadCoilsRequest(address, quantity),
unitId);
ReadCoilsResponse readCoilsResponse = future.get();
if (readCoilsResponse != null) {
ByteBuf buf = readCoilsResponse.getCoilStatus();
result = buf.readBoolean();
ReferenceCountUtil.release(readCoilsResponse);
}
return result;
}
/**
* Desc:
* 读取readDiscreteInputs开关量
*
* @author JoyBlack
* @email 1016037677@qq.com
* @date 2023/1/5 上午 10:04
*/
public Boolean readDiscreteInputs(int address, int quantity, int unitId)
throws Exception {
Boolean result = null;
CompletableFuture<ReadDiscreteInputsResponse> future = master
.sendRequest(new ReadDiscreteInputsRequest(address, quantity), unitId);
ReadDiscreteInputsResponse discreteInputsResponse = future.get();
if (discreteInputsResponse != null) {
ByteBuf buf = discreteInputsResponse.getInputStatus();
result = buf.readBoolean();
ReferenceCountUtil.release(discreteInputsResponse);
}
return result;
}
public Result<Map<Integer, Number>> run(int start, int end, int quantity, int unitId) {
Map<Integer, Number> result = new HashMap<>();
try {
for (int i = start; i <= end; i += 2) {
Number value = readHoldingRegister(i, quantity, unitId);
if ("infinity".equals(value.toString().toLowerCase())) {
logger.error(String.format("点位[%s]数据转换失败,设置为默认值 null", i));
result.put(i, null);
} else {
result.put(i, value);
}
}
return new Result<Map<Integer, Number>>().ok(result);
} catch (Exception e) {
String msg = String.format("【监控数据采集任务】获取监控数据失败:%s, start = %d, end = %d.", e.getMessage(), start, end);
logger.error(msg);
Result<Map<Integer, Number>> error = new Result<Map<Integer, Number>>().error(msg);
return error;
}
}
public static void main(String[] args) {
try {
ModBus modBus = new ModBus("127.0.0.1", 502);
for (int i = 0; i < 10; i++) {
Result<Map<Integer, Number>> run = modBus.run(0, 9, 2, 1);
System.out.println(run.getData());
Thread.sleep(1000);
}
// 释放资源
modBus.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
辅助返回类
package com.example.modbustest.md;
import java.io.Serializable;
/**
* 响应数据
*/
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 编码:0表示成功,其他值表示失败
*/
private int code = 0;
/**
* 消息内容
*/
private String msg = "success";
/**
* 响应数据
*/
private T data;
public Result<T> ok(T data) {
this.setData(data);
return this;
}
public boolean success() {
return code == 0;
}
public Result<T> error() {
return this;
}
public Result<T> error(int code) {
return this;
}
public Result<T> error(int code, String msg) {
this.code = code;
this.msg = msg;
return this;
}
public Result<T> error(String msg) {
this.code = 500;
this.msg = msg;
return this;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
2、配置数据生成(slave端)
主程序编写完成后,需要启动一个slave端,方便主程序读取数据。我们使用modbus poll充当。
1、安装modbus poll:下载附录指定的程序后,执行安装。
2、运行程序,可以得到如下图所示的界面
3、右键内容页面,选择Slave Definition,设置slave寄存器数量、读取数据方式(F=03)
4、点击OK后,初始化为10行的内容页
5、右键存储数据单元格:设置数据类型、大小端模式,我们一般设置为Float(占用两个寄存器,4个字节)以及大端模式
6、双击存储数据单元格:设置数据内容、自增(动态变化方便客户端查询)
点击OK,完成设置。发现数据在不停的增长,通过此方式可以依次设置不同地址的寄存器存储的数据。由于此数据使用了两个寄存器(地址0和地址1),因此我们只能从地址为2的寄存器开始设置.如下图所示:
7、按照步骤5和6,依次设置剩余4个数值的数据,刚好使用玩完10个寄存器(我们的采集程序刚好采集5个数值,对应0-9 10个寄存器地址)
3、测试数据读取
1、点击java程序中的ModBus类,执行其main方法,该程序会间隔1秒读取10次本地配置的modbus poll生成的10个数值,执行结果如下图所示:
结语
本文只是做了一个简单介绍,若需要将程序应用到实际项目,还需要将其进行拓展,比如数据的读取频率、多线程辅助、消息缓冲队列还实现实际的生产任务。相关代码、软件资源,可参考附录部分。
附录
- modbus poll 安装程序:链接: https://pan.baidu.com/s/1a5eZpwYbCy60XuM12PlMpQ?pwd=zhao 提取码: zhao
- 代码地址:https://github.com/joyblack/modbus-test.git