最近因为项目的需求,需要用到modbus TCP/IP协议,听到这个名字感觉很熟悉,毕竟我们常用的http协议也是基于TCP/IP封装的。带着这种既熟悉又陌生的感觉,开始在网上查找一些资料,发现网上关于这方面的资料不是很多,而且也不是特别全。所以趁现在有时间就整理一下项目中这个模块的指示,方便自己以后查看。
首先我们简单了解一下什么是modbus TCP/IP协议。modbus是由Modicon(现为施耐德电气公司的一个品牌)在1979年发明的,是全球第一个真正用于工业现场的总线协议。modbus协议是应用于电子控制器上的一种通用语言。通过这个协议,控制器相互之间,控制器经由网络(例如以太网)和其它设备之间可以通信。modbus本身就是一个通信协议,可以基于串口,也可以基于网口。基于串口的由RTU,基于网口的由TCP,默认端口号502。通常我们把服务器端作为主站,将带有modbus模块的设备作为从站处理。利用modbus从寄存器中读取和写数据。
功能码 名称 作用
01 读取线圈状态 取得一组逻辑线圈的当前状态(ON/OFF)
02 读取输入状态 取得一组开关输入的当前状态(ON/OFF)
03 读取保持寄存器 在一个或多个保持寄存器中取得当前的二进制值
04 读取输入寄存器 在一个或多个输入寄存器中取得当前的二进制值
05 强置单线圈 强置一个逻辑线圈的通断状态
06 预置单寄存器 把具体二进值装入一个保持寄存器
07 读取异常状态 取得8个内部线圈的通断状态,这8个线圈的地址由控制器决定
08 回送诊断校验 把诊断校验报文送从机,以对通信处理进行评鉴
09 编程(只用于484) 使主机模拟编程器作用,修改PC从机逻辑
10 控询(只用于484) 可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码9的报文发送后,本功能码才发送
11 读取事件计数 可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时
12 读取通信事件记录 可是主机检索每台从机的ModBus事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误
13 编程(184/384 484 584) 可使主机模拟编程器功能修改PC从机逻辑
14 探询(184/384 484 584) 可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能13的报文发送后,本功能码才得发送
15 强置多线圈 强置一串连续逻辑线圈的通断
16 预置多寄存器 把具体的二进制值装入一串连续的保持寄存器
17 报告从机标识 可使主机判断编址从机的类型及该从机运行指示灯的状态
18 (884和MICRO 84) 可使主机模拟编程功能,修改PC状态逻辑
19 重置通信链路 发生非可修改错误后,是从机复位于已知状态,可重置顺序字节
20 读取通用参数(584L) 显示扩展存储器文件中的数据信息
21 写入通用参数(584L) 把通用参数写入扩展存储文件,或修改之
22~64 保留作扩展功能备用
65~72 保留以备用户功能所用 留作用户功能的扩展编码
73~119 非法功能
120~127 保留 留作内部作用
128~255 保留 用于异常应答
在Android的开发中用到的可能就是读取和写入功能。接下来我们就开始代码开发。
首先我们需要导入相关的jar包,主要有两个modbus4j.jar,serroUtils.jar。
导入了需要的jar包之后,我们就开始和modbus建立联系了。代码如下:
-
import java.util.Date;
-
import com.serotonin.modbus4j.ModbusFactory;
-
import com.serotonin.modbus4j.ModbusMaster;
-
import com.serotonin.modbus4j.exception.ModbusInitException;
-
import com.serotonin.modbus4j.exception.ModbusTransportException;
-
import com.serotonin.modbus4j.ip.IpParameters;
-
import com.serotonin.modbus4j.msg.ModbusRequest;
-
import com.serotonin.modbus4j.msg.ModbusResponse;
-
import com.serotonin.modbus4j.msg.ReadHoldingRegistersRequest;
-
import com.serotonin.modbus4j.msg.WriteRegistersRequest;
-
import com.serotonin.modbus4j.msg.WriteRegistersResponse;
-
import com.serotonin.util.queue.ByteQueue;
-
public class ReadAWriteUtil {
-
public static void modbusWTCP(String ip, int port, int slaveId, int start, short[] values) {
-
ModbusFactory modbusFactory = new ModbusFactory();
-
// 设备ModbusTCP的Ip与端口,如果不设定端口则默认为502
-
IpParameters params = new IpParameters();
-
params.setHost(ip);
-
if (502 != port) {
-
params.setPort(port);
-
}// 设置端口,默认502
-
ModbusMaster tcpMaster = null;
-
// 参数1:IP和端口信息 参数2:保持连接激活
-
tcpMaster = modbusFactory.createTcpMaster(params, true);
-
try {
-
tcpMaster.init();
-
System.out.println("===============" + 1111111);
-
} catch (ModbusInitException e) {
-
// System.out.println("11111111111111=="+"此处出现问题了啊!");
-
// 如果出现了通信异常信息,则保存到数据库中
-
//CommunityExceptionRecord cer = new CommunityExceptionRecord();
-
//cer.setDate(new Date());
-
//cer.setIp(ip);
-
// cer.setRemark(bgName+"出现连接异常");
-
// batteryGroupRecordService.saveCommunityException(cer);
-
}
-
try {
-
WriteRegistersRequest request = new WriteRegistersRequest(slaveId, start, values);
-
WriteRegistersResponse response = (WriteRegistersResponse) tcpMaster.send(request);
-
if (response.isException())
-
System.out.println("Exception response: message=" + response.getExceptionMessage());
-
else
-
System.out.println("Success");
-
} catch (ModbusTransportException e) {
-
e.printStackTrace();
-
}
-
}
-
public static ByteQueue modbusTCP(String ip, int port, int start,int readLenth) {
-
ModbusFactory modbusFactory = new ModbusFactory();
-
// 设备ModbusTCP的Ip与端口,如果不设定端口则默认为502
-
IpParameters params = new IpParameters();
-
params.setHost(ip);
-
if(502!=port){params.setPort(port);}//设置端口,默认502
-
ModbusMaster tcpMaster = null;
-
tcpMaster = modbusFactory.createTcpMaster(params, true);
-
try {
-
tcpMaster.init();
-
System.out.println("==============="+1111111);
-
} catch (ModbusInitException e) {
-
return null;
-
}
-
ModbusRequest modbusRequest=null;
-
try {
-
modbusRequest = new ReadHoldingRegistersRequest(1, start, readLenth);//功能码03
-
} catch (ModbusTransportException e) {
-
e.printStackTrace();
-
}
-
ModbusResponse modbusResponse=null;
-
try {
-
modbusResponse = tcpMaster.send(modbusRequest);
-
} catch (ModbusTransportException e) {
-
e.printStackTrace();
-
}
-
ByteQueue byteQueue= new ByteQueue(12);
-
modbusResponse.write(byteQueue);
-
System.out.println("功能码:"+modbusRequest.getFunctionCode());
-
System.out.println("从站地址:"+modbusRequest.getSlaveId());
-
System.out.println("收到的响应信息大小:"+byteQueue.size());
-
System.out.println("收到的响应信息值:"+byteQueue);
-
return byteQueue;
-
}
-
}
通过运行以上代码,就可以看到我们已经和modbus建立起了相关的联系,并且可以写入和读取数据了。但着才仅仅只是个开始。对于习惯了返回数据为json字符串的我们,当看到控制台打印出的byte数组时一定是一脸的懵逼,what is it?所以接下来我们就讲一讲modbus tcp的数据含义。在modbus开发的时候,Android和plc之间会定义好一写地址,并标明地址的意义和相关数据的含义。主要分为两类地址,一种是写入类型地址,一种是读取类型地址。如下:
写入数据地址:
268 | %MX | 402.5 | BOOL | 1 bit | APP_DISLIGHT_DI | 表示的含义 |
402.5是地址,Bool是数据类型,1bit是数据所占的位数,%MX表示以字节为单位
在plc中,整形数据都是short类型,占2个字节。而我们在通讯过程中传入的起始地址参数是int类型的,占4个字节。所以我们在看到402.5(代表第402个字节的第5位)这个地址的时候需要做一个除以2的处理。至于原因,应该是其转换为2进制是一样的。所以我们传入的startadr应该是201+偏移量(这个值是plc设定好的已知量),至于第五位就是我们需要传入的value值,那我们需要传入多少呢,因为这个数据类型是bool,所以是由0或者1两种值,如果传入0的话就直接传0就可以了,如果传入1的话就需要传入2的5次方,转换为二进制之后你就能看到在第五位的值是1了。
再来看看读取地址:
11 | %MX | 287.2 | BOOL | 1 bit | APP_CHECK_WATERIN_FINISHED | 表示含义 |
287.2是地址,Bool是数据类型,1bit是数据所占的位数,%MX表示以字节为单位
读取的起始地址同上需要做除以2的处理,第二个参数为读取的长度,是以short类型为单位的,就说如果读取长度为1,那就是读取一个short,2个字节的数据。在读取到数据之后,我们还需要把数据转换为二进制,然后根据相关位上的数据来判断具体的含义。这里还有一个问题就是转换出来的二进制和本身含义是相反的,就是说二进制的第一位应该对应从起始地址开始的最后一位的含义。