本文记录使用java开发modbus协议程序,与串口进行连接,使用到的开源代码是modbus4j。
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。
demo代码路径:
https://gitee.com/crazybytex/modbustmp
依赖下载
rxtxcomm
modbus4j
rxtxcom
http://fizzed.com/oss/rxtx-for-java
下载后,注意查看说明:
Windows
Choose your binary build - x64 or x86 (based on which version of
the JVM you are installing to)
NOTE: You MUST match your architecture. You can't install the i386
version on a 64-bit version of the JDK and vice-versa.
For a JDK installation:
Copy RXTXcomm.jar ---> <JAVA_HOME>\jre\lib\ext
Copy rxtxSerial.dll ---> <JAVA_HOME>\jre\bin
Copy rxtxParallel.dll ---> <JAVA_HOME>\jre\bin
Linux
Choose your binary build - x86_64 or i386 (based on which version of
the JVM you are installing to)
NOTE: You MUST match your architecture. You can't install the i386
version on a 64-bit version of the JDK and vice-versa.
For a JDK installation on architecture=i386
Copy RXTXcomm.jar ---> <JAVA_HOME>/jre/lib/ext
Copy librxtxSerial.so ---> <JAVA_HOME>/jre/lib/i386/
Copy librxtxParallel.so ---> <JAVA_HOME>/jre/lib/i386/
NOTE: For a JDK installation on architecture=x86_64, just change the
i386 to x86_64 above.
PS:
如果按照上面的设置,还会提示找不到jar ,可以简单粗暴的直接放在lib目录下
modbus4j
https://github.com/MangoAutomation/modbus4j
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
ps:
也可以自己直接下载源代码,直接进行打包使用,然后jar放在lib下
注意,有些maven镜像仓库可能下载不到这个依赖
串口启动程序
modbus4j 没有串口相关的实现,需要自行实现相关功能。
demo实现思路:
参数配置:
com/crazybytex/modbus/util/SerialPortParameter.java
核心代码:
/**
* 串口名称(COM0、COM1、COM2等等)
*/
private String serialPortName;
/**
* 波特率
* 默认:9600
*/
private int baudRate;
/**
* 数据位 默认8位
* 可以设置的值:SerialPort.DATABITS_5、SerialPort.DATABITS_6、SerialPort.DATABITS_7、SerialPort.DATABITS_8
* 默认:SerialPort.DATABITS_8
*/
private int dataBits;
/**
* 停止位
* 可以设置的值:SerialPort.STOPBITS_1、SerialPort.STOPBITS_2、SerialPort.STOPBITS_1_5
* 默认:SerialPort.STOPBITS_1
*/
private int stopBits;
/**
* 校验位
* 可以设置的值:SerialPort.PARITY_NONE、SerialPort.PARITY_ODD、SerialPort.PARITY_EVEN、SerialPort.PARITY_MARK、SerialPort.PARITY_SPACE
* 默认:SerialPort.PARITY_NONE
*/
private int parity;
串口工具类:
com/crazybytex/modbus/util/SerialPortUtil.java
核心代码:
public static SerialPort openSerialPort(SerialPortParameter parameter)
throws NoSuchPortException, PortInUseException, UnsupportedCommOperationException {
return openSerialPort(parameter, 2000);
}
public static void closeSerialPort(SerialPort serialPort) {
if (serialPort != null) {
serialPort.close();
System.out.println("Port closed" + serialPort.getName());
}
}
实现类:
com/crazybytex/modbus/SerialPortWrapperImpl.java
核心代码:
private SerialPort serialPort;
private SerialPortParameter parameter;
@Override
public void close() throws Exception {
SerialPortUtil.closeSerialPort(serialPort);
}
@Override
public void open() throws Exception {
serialPort = SerialPortUtil.openSerialPort(parameter);
}
详细代码请参考:
https://gitee.com/crazybytex/modbustmp
modbus4j 项目说明
项目源码中,作者已经有了不少示例测试程序了,实现的时候可以参考
对于modbus协议开发,无非就是按照地址,功能码对寄存器进行读取,作者已经提供了相关功能码的信息发送。
比如
所以使用modbus4j 与设备通信,就转变成了使用相关请求和响应实体,调用相关方法而已。
对于response 提供了获取字节、或者short数组、或者boolean值的快捷方法
/*
* ============================================================================
* GNU General Public License
* ============================================================================
*
* Copyright (C) 2006-2011 Serotonin Software Technologies Inc. http://serotoninsoftware.com
* @author Matthew Lohbihler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.msg;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.sero.io.StreamUtils;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>Abstract ReadResponse class.</p>
*
* @author Matthew Lohbihler
* @version 5.0.0
*/
abstract public class ReadResponse extends ModbusResponse {
private byte[] data;
ReadResponse(int slaveId) throws ModbusTransportException {
super(slaveId);
}
ReadResponse(int slaveId, byte[] data) throws ModbusTransportException {
super(slaveId);
this.data = data;
}
/** {@inheritDoc} */
@Override
protected void readResponse(ByteQueue queue) {
int numberOfBytes = ModbusUtils.popUnsignedByte(queue);
if (queue.size() < numberOfBytes)
throw new ArrayIndexOutOfBoundsException();
data = new byte[numberOfBytes];
queue.pop(data);
}
/** {@inheritDoc} */
@Override
protected void writeResponse(ByteQueue queue) {
ModbusUtils.pushByte(queue, data.length);
queue.push(data);
}
/**
* <p>Getter for the field <code>data</code>.</p>
*
* @return an array of {@link byte} objects.
*/
public byte[] getData() {
return data;
}
/**
* <p>getShortData.</p>
*
* @return an array of {@link short} objects.
*/
public short[] getShortData() {
return convertToShorts(data);
}
/**
* <p>getBooleanData.</p>
*
* @return an array of {@link boolean} objects.
*/
public boolean[] getBooleanData() {
return convertToBooleans(data);
}
/**
* <p>toString.</p>
*
* @param numeric a boolean.
* @return a {@link java.lang.String} object.
*/
public String toString(boolean numeric) {
if (data == null)
return "ReadResponse [null]";
return "ReadResponse [len=" + (numeric ? data.length / 2 : data.length * 8) + ", " + StreamUtils.dumpHex(data)
+ "]";
}
}
所以可以直接使用这几个快捷方法,获取相关的字节或者short
为什么是short数组?
因为可以进行连续的读多个地址的数据,那么自然可能会返回多个数据,而不是单个short
本文作者:程序员潇然 疯狂的字节X https://crazybytex.com/
编程范式
与modbus通信的逻辑就是:
对指定设备Id的设备,指定某个地址,根据功能码,读取指定个数的数据。
使用modbus4j的编程范式就是:
使用项目提供的相关请求和响应实体(内部封装了功能码),指定设备ID以及地址之后,就可以进行数据读取了。
比如提供的样例如下:
public static void readDiscreteInputTest(ModbusMaster master, int slaveId, int start, int len) {
try {
ReadDiscreteInputsRequest request = new ReadDiscreteInputsRequest(slaveId, start, len);
ReadDiscreteInputsResponse response = (ReadDiscreteInputsResponse) master.send(request);
if (response.isException())
System.out.println("Exception response: message=" + response.getExceptionMessage());
else
System.out.println(Arrays.toString(response.getBooleanData()));
}
catch (ModbusTransportException e) {
e.printStackTrace();
}
}
改造应用
我的测试项目场景为,大概率读取多个连续的地址,将这几个地址的数据解析到javaBean
通用方法(功能码为4的)如下,ModBusResponse 是自定义的返回实体类
如果是读多个连续地址,直接通过ModBusResponse的子类实现parse方法,解析到具体的字段
如果是读单个地址,或者不实现ModBusResponse,不重写parse方法,可以直接解析返回的字节数组
public static byte[] readInputRegisters(ModbusMaster master, int slaveId, int start, int len, ModBusResponse modBusResponse) {
try {
ReadInputRegistersRequest request = new ReadInputRegistersRequest(slaveId, start, len);
System.out.println("request data:"+request);
ReadInputRegistersResponse response = (ReadInputRegistersResponse) master.send(request);
if (response.isException()) {
modBusResponse.setCode(Constant.RET_CODE_FAILED);
modBusResponse.setMsg(response.getExceptionMessage());
return null;
}
System.out.println("received bytes:"+ByteStreamUtils.bytesToHex(response.getData()));
System.out.println("received short array:"+Arrays.toString(response.getShortData()));
modBusResponse.parse(response.getData());
System.out.println("final response data:"+modBusResponse);
System.out.println();
return response.getData();
} catch (ModbusTransportException e) {
e.printStackTrace();
}
return null;
}
package com.crazybytex.modbus.beans;
import com.serotonin.modbus4j.ModbusMaster;
/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/3 17:22
* @Description 请求实体封装
**/
public class ModBusRequest {
private ModbusMaster modbusMaster;
private int slaveId;
private int startOffset;
private int numberOfRegisters;
public ModBusRequest(){
}
public ModBusRequest(ModbusMaster modbusMaster,int slaveId,int startOffset,int numberOfRegisters){
this.modbusMaster = modbusMaster;
this.slaveId = slaveId;
this.startOffset = startOffset;
this.numberOfRegisters = numberOfRegisters;
}
/**
* 不设置numberOfRegisters 默认为 1
* @param modbusMaster
* @param slaveId
* @param startOffset
*/
public ModBusRequest(ModbusMaster modbusMaster,int slaveId,int startOffset){
this.modbusMaster = modbusMaster;
this.slaveId = slaveId;
this.startOffset = startOffset;
this.numberOfRegisters = 1;
}
public ModbusMaster getModbusMaster() {
return modbusMaster;
}
public void setModbusMaster(ModbusMaster modbusMaster) {
this.modbusMaster = modbusMaster;
}
public int getSlaveId() {
return slaveId;
}
public void setSlaveId(int slaveId) {
this.slaveId = slaveId;
}
public int getStartOffset() {
return startOffset;
}
public void setStartOffset(int startOffset) {
this.startOffset = startOffset;
}
public int getNumberOfRegisters() {
return numberOfRegisters;
}
public void setNumberOfRegisters(int numberOfRegisters) {
this.numberOfRegisters = numberOfRegisters;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ModBusRequest{");
sb.append("modbusMaster=").append(modbusMaster);
sb.append(", slaveId=").append(slaveId);
sb.append(", startOffset=").append(startOffset);
sb.append(", numberOfRegisters=").append(numberOfRegisters);
sb.append('}');
return sb.toString();
}
}
package com.crazybytex.modbus.beans;
import static com.crazybytex.modbus.Constant.RET_CODE_SUCCESS;
/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/3 17:23
* @Description TODO
**/
public class ModBusResponse {
private int code;
private String msg;
public ModBusResponse() {
code = RET_CODE_SUCCESS;
msg="success";
}
public ModBusResponse(int code,String msg) {
this.code = code;
this.msg = msg;
}
/**
* 解析返回的字节数组 转换为对应的实体类 默认什么都不做 需要处理子类进行
* @param bytes 需要处理的字节数组
*/
public void parse(byte[] bytes){
}
public int code() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String msg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ModBusResponse{");
sb.append("code=").append(code);
sb.append(", msg='").append(msg).append('\'');
sb.append('}');
return sb.toString();
}
}
一个子类实现
package com.crazybytex.modbus.beans;
import com.crazybytex.modbus.util.ByteStreamUtils;
import java.io.ByteArrayInputStream;
/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/3 17:47
* @Description 电池负载电压和电流
* 功能 地址 功能码 描述 单位 倍数
* ------------------------------------------------------------------------------------------------
*
*负载电压 304A (0d12362) 04 (只读) 负载电压 V 100
*负载电流 304B (0d12363) 04 (只读) 负载电流 A 100
*
*
*
**/
public class BatteryLoadVoltageAndCurrentResponse extends ModBusResponse{
/**
* 蓄电池电压
*/
private int batteryLoadVoltage;
/**
* 蓄电池电流
*/
private double batteryLoadCurrent;
@Override
public void parse(byte[] bytes) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
batteryLoadVoltage = ByteStreamUtils.readShort(byteArrayInputStream)/100;
batteryLoadCurrent = ByteStreamUtils.readShort(byteArrayInputStream)/100.00;
}catch(Exception e){
}
}
public int getBatteryLoadVoltage() {
return batteryLoadVoltage;
}
public void setBatteryLoadVoltage(int batteryLoadVoltage) {
this.batteryLoadVoltage = batteryLoadVoltage;
}
public double getBatteryLoadCurrent() {
return batteryLoadCurrent;
}
public void setBatteryLoadCurrent(double batteryLoadCurrent) {
this.batteryLoadCurrent = batteryLoadCurrent;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("BatteryLoadVoltageAndCurrentResponse{");
sb.append("batteryLoadVoltage=").append(batteryLoadVoltage);
sb.append(", batteryLoadCurrent=").append(batteryLoadCurrent);
sb.append('}');
return sb.toString();
}
}
封装的方法调用:
package com.crazybytex.modbus.service;
import com.serotonin.modbus4j.ModbusMaster;
import com.crazybytex.modbus.beans.BatteryLoadVoltageAndCurrentResponse;
import com.crazybytex.modbus.util.ModbusUtil;
import static com.crazybytex.modbus.Constant.*;
/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/4 9:42
* @Description TODO
**/
public class BatteryService {
public BatteryLoadVoltageAndCurrentResponse getLoadVoltageAndCurrent(ModbusMaster client, int slaveId){
BatteryLoadVoltageAndCurrentResponse batteryLoadVoltageAndCurrentResponse = new BatteryLoadVoltageAndCurrentResponse();
ModbusUtil.readInputRegisters(client,slaveId,ADDRESS_LOAD_VOLTAGE,2,batteryLoadVoltageAndCurrentResponse);
return batteryLoadVoltageAndCurrentResponse;
}
}
这样就可以直接通过javabean获取返回结果了
完整代码示例:
https://gitee.com/crazybytex/modbustmp
转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-128-1-1.html