hey-girl东拼西凑原创文章,若有歧义可留言,若需转载需标明出处
前言:接上篇继续说说modbus4j的几个常用方法.已经代码分析
项目中使用MouBus4j
说说实际开发中。首先创建一个springboot项目。pom引入依赖如下:
<repositories>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>ias-snapshots</id>
<name>Infinite Automation Snapshot Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-snapshot/</url>
</repository>
<repository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>ias-releases</id>
<name>Infinite Automation Release Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-release/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.scream3r</groupId>
<artifactId>jssc</artifactId>
<version>2.8.0</version>
</dependency>
</dependencies>
下面针对modbus4j几个常用方法进行分析,这样才能保证开发起来得心应手。程序为master(主),slave工具模拟从设备。
在开始代码分析前:首先说说modbus4j这个库常用核心的类代表啥。参考文章
- 主机Master及其子类:主机的入口,数据流的起点和终点。
- 数据端口类StreamTransport:负责数据的写入和读出。
- Modbus消息类ModbusMessage及其子类:支持Modbus定义的各种方法(FunctionCode)
- 收发数据控制类MessageControl:支持 timeout、retries,默认200ms,1次。
- 收发等待室WaitingRoom:负责同步收发逻辑。
- 输出Request消息类:OutgoingRequestMessage 及其子类。
- 收到Response消息类:IncomingResponseMessage 及其子类。
- 解析类MessageParser:负责解析收到的消息。
- 协议数据类型定义:DataType
- 协议功能码定义:FunctionCode
- 协议寄存器范围:RegisterRange
A---- readHoldingRegisters方法,读保持寄存器。调用这个方法 readHoldingRegisters(master,1,0,4) 进行分析。
public void readHoldingRegisters(ModbusMaster master, int slaveId, int start, int len) {
try {
// 关键代码
ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(slaveId, start, len);
ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) master.send(request);
if (response.isException()){
log.error("Exception response: message=" + response.getExceptionMessage());
} else{
log.info(Arrays.toString(response.getShortData()));
}
}
catch (ModbusTransportException e) {
e.printStackTrace();
}
finally {
master.destroy();
}
}
DemoA整体流程解析:
-
ModbusFactory类创建对应的Master对象。这里用到的是子类RtuMaster
-
构建ReadHoldingRegistersRequest(继承ModbusRequest)请求对象。里面就是包含从机地址,保持寄存器地址和数量等信息。ModbusRequest有很多子类。若我们需求不是读保持寄存器。就找对应的子类构建就可以
-
调用ModbusMaster(抽象类)的send(ModbusRequest request)方法。先校验数据,主要是看看你给request的参数是不是符合需求。然后会调用this.sendImpl(request),这其实就是调用的他的实现子类RtuMaster的sendImpl(ModbusRequest request)方法。这个方法的返回就是ModbusResponse。这方法做的就是发消息了会调用MessageControl的send。方法返回的是RtuMessageResponse。
-
-
MessageControl的send方法就会发送报文。并接收报文。
-
这里我们收到的数据应该是正常报文,就是从机地址,功能码,数据字节数,数据,校验这种格式。但是MessageControl会做返回数据的处理。
-
MessageParser接口的parseMessage方法,是对消息处理的。实现类RtuMessageParser的parseMessageImpl是实际干活的会调用
RtuMessageResponse.createRtuMessageResponse(queue)
这里queue是原始报文数据,RtuMessageResponse的创建需要ModbusResponse对象。所以这里createModbusResponse方法是关键,它会把从机地址,功能码以及校验去掉。还会调用((ModbusResponse)response).read(queue, isException);执行的就是readResponse这个抽象方法。这个方法会把原始数据进行处理。
这么描述只是让你大概熟悉流程,具体的代码解析,请往下看。
Q1-代码解析ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(slaveId, start, len)
A1- ReadHoldingRegistersRequest 继承 ReadNumericRequest 继承 ModbusRequest 继承ModbusMessage。创建一个ReadHoldingRegistersRequest对象的过程主要是验证slaveId的值是不是不符合规则的。然后保持寄存器起始位置和寄存器数量赋值的过程。也就是start是保持寄存器的起始位置。len表示的是保持寄存器数量。
Q1-(ReadHoldingRegistersResponse) master.send(request)
A1- master就是创建的RtuMaster对象。主要看send方法。先执行ModbusMaster里的send方法。
这里会调用validate方法。这个方法是ModbusRequest的抽象方法。
他的实现有10个。如图:
所以就来到ReadNumericRequest 的validate方法。
这个方法主要做就是对保持寄存器的这个起始位置值做了校验(取值合法范围0-65535)。对读寄存器的数量也进行校验。以及对读取的起始位置+读的数量。判断是否合规。就理解为发请求前对参数的校验。
在看他的第二个方法sendImpl
,接收一个ModbusRequest参数。这个方法是它自己本身的抽象方法。
有5个具体实现
这里我们是RTU模式。主要看看rtu
这个方法主要是把传入的request对象包装成RtuMessageRequest对象。this.conn是MessageControl对象。然后调用send方法。
send方法主要是把包装的request里面的参数变成byte[].也就是包装成发送的报文。
这串报文看着就是我们熟悉的标准rtu报文帧,01也就是从机地址,03功能码和data以及校验位2个字节。然后循环write发送。也就是和我们最开始的传参符合,报文就是读从机地址01,保持寄存器起始位置0,保持寄存器数量4.
我们看看响应,正常响应的报文应该是如下
01 03 08 00 00 00 00 00 16 00 00 74 13 (01从机地址,03功能码,08数据域字节数,00 00 00 00 00 16 00 00数据高低位 ,74 13校验)。正好对应从设备。(注意这里16是hex对应dec就是22)
转看程序返回的是一个RtuMessageResponse对象,extends RtuMessage implements OutgoingResponseMessage, IncomingResponseMessage。
RtuMessageResponse的创建接收一个ModbusResponse对象。ModbusResponse extends ModbusMessage。他是一个抽象类。这里要用到他的实现类ReadHoldingRegistersResponse。
他接收的参数queue,就是响应的原始报文。
经过一系列构建方法以后,会得到RtuMessageResponse对象。它里的的data就是数据位。已经从16进制转换成10进制了。这里就是我们读的数据
最终返回结果[0, 0, 22, 0]
B----readHoldingRegistersByLocator的方法。(这个方法也是对保持寄存器进行读取)进行分析。
方法调用readHoldingRegistersByLocator(master,1,0, DataType.FOUR_BYTE_INT_UNSIGNED)。
这里也master是用的RtuMaster
1 :从机地址
0:保持寄存器起始 地址
这个方法:从从机地址为1,保持寄存器地址0。
DataType.FOUR_BYTE_INT_UNSIGNED:这个参数很重要,往后看。后面讲了的。
public void readHoldingRegistersByLocator(ModbusMaster master, int slaveId, int offset, int dataType) {
try{
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, offset, dataType);
System.out.println(master.getValue(locator));
}catch (ModbusTransportException | ErrorResponseException e) {
}
}
Q1-代码解析BaseLocator.holdingRegister(slaveId, 0, DataType.EIGHT_BYTE_INT_UNSIGNED);
A1- BaseLocator是个抽象类。这里调用的它的静态方法holdingRegister方法,接收三个参数。slaveId表示从机地址,0就是保持寄存器起始地址,DataType.FOUR_BYTE_INT_UNSIGNED的值这里是4。这个方法主要是做了一件事创建创建NumericLocator对象,new NumericLocator(slaveId, 3, offset, dataType);
NumericLocator继承BaseLocator抽象类。构建方法接收4个参数
这里分别是slaveId从机地址,range 功能码,offset偏移量。dataType数据类型。构造方法做的事情就是赋值并且校验datatype的合法性。在modbus4j3.0.6-SNAPSHOT版本中。参数range的取值范围是如下:
执行完以后会得到一个BaseLocator对象。如下:
Q2-代码解析master.getValue(locator)
A2- getvalue 其实还是调用了ModbusMaster的send方法。
注意这个方法,会得到一个List<ReadFunctionGroup>集合。对这个集合进行迭代执行sendFunctionGroup方法
如下图:
sendFunctionGroup这个方法。根据FunctionCode就是就功能码,判断当前是什么类型请求。这里是读保持寄存器,也就是03。创建ReadHoldingRegistersRequest对象(这个对象是不是很眼熟。不就是我们第一个demoA,在send的时候用到的请求对象哈)。有了这个request对象就可以执行send方法。就和demoA完全一样的请求流程。
本例中请求的报文为如下图:
这个报文就是和前面相呼应从机地址01 ,功能码03,保持寄存器起始地址0,保持寄存器数量2
响应报文如下:
这句报文对应的就是01从机地址,03功能码,04数据的字节数,00 58 00 00数据。58就是十进制的88
但是modbus4j做了返回的报文封装。我们程序执行到最后得到的是5767168这个值
为什么会是这个值,想想我们本例的需求。读保持寄存器0位置,数量读2个。按理来说2个寄存器得给我返回4byte.一个寄存器里存的是1word(等价于2byte)。为啥结果是5767168。还得从我们调用方法的时候给的参数DataType.FOUR_BYTE_INT_UNSIGNED说起。
这个DataType 在 package com.serotonin.modbus4j.code。大家可以去看看源码。
理解也好理解。因为对应的设备不一样。modbus规定了一个寄存器里存的是1word。这没毛病。但是你想想。万一硬件的产品是4个字节或者8个字节存储一个值。这个值又和它约定有关。
所以我们这个需求的意思就是从机地址01,读保持寄存器00开始,保持寄寄存器数量2。读到了4个字节。我这个字节表示的是一个值。比如我们这里的。
00 58 00 00 这是16进制表示。我们给的参数是FOUR_BYTE_INT_UNSIGNED。换算成二进制等于
00000000 01011000 00000000 00000000。
这个2进制换算成10进制就是5767168
还有点说明的。我们前面并没有告诉方法说要读保持寄存器的数量。这个数量2的由来
int length = functionGroup.getLength();
这个length来自DataType的getRegisterCount方法
和demoA的区别就是:sendFunctionGroup这个方法后面执行了。
这么一句results.addResult(locator.getKey(), locator.bytesToValue(data, startOffset));
而demoA是直接将报文结果进制转换后返回。
这句代码最终干活的是bytesToValueRealOffset这个方法。也就是根据datatype进行转换
总结:虽然写的有点潦草,因为边学边写有点赶时间,但是理就是这样的。懂得都懂