基于ModbusTcp协议的Java Socket通信 报文编码格式与数据采集过程详解(下)

后续可以进一步实现更多的功能,适应实际环境中的通信:

 1. 如果数据发送不连续
 2. 如果收到的数据有误 解析不了 报错?
 3. 批量读且解析
 4. 修改tcp寄存器的数值
 5. 如果换个协议 怎么尽可能减少代码的修改 融入设计模式

1. 数据发送不连续 阻塞等待 设置接收长度判断和超时判断
在这里插入图片描述

2. 要查看配置是否有问题:
1个寄存器数量=1个长度=16位=2个字节 unsigned short
而浮点数是4个字节 32位需要2个寄存器去存放 int/uint也是4个字节


带浮点数的进制转化:
c9 41 9a 99 -> 41 c9 99 9a(但并不是简单地拆分计算)
以四个字节的数据为准:4*8=32位 31位为符号,30到23八个字节为阶码,由第二步计算得来,尾数部分则是剩余的小数部分(由后23位决定)
1.将经纬度数据(10进制,浮点型)转换成二进制形式;
2.小数点向左移动n位,直到最后一个1停止,得到左移的位数,计算阶码,127+(左移位数);
3.尾数(22到0位置总计23个位数)为小数部分的二进制编码,剩余未填满的位数用0补齐;
4.再将所有得到的2进制编码,转换成16进制的数据。

e.g.
首先将6.91转换为二进制形式:110.111010001111010111000
将其规范化:调整使其实数第一位大于1小于2
6.91 = 1.10111010001111010111000 * 2^2
所以S:0
EXP : 2(小数点左移位数)+127(10进制) =129(10进制) = 10000001(2进制)
Fraction : 10111010001111010111000 (注意:小数点前面的1不要了)
组合一下: 0 10000001 10111010001111010111000
= 0100 0000 1101 1101 0001 1110 1011 1000= 4 0 D D 1 E B 8

在线转换网页:https://lostphp.com/hexconvert/


3.批量读并解析:

多条指令
在这里插入图片描述
一条指令
在这里插入图片描述


4.修改寄存器的数值:

在这里插入图片描述

多条指令一起修改
多条指令一起修改


读写读三遍比对:
在这里插入图片描述
注:如果要修改的值超出字符范围大小,则将全部字节进行(byte)强制转换。


5. 面向对象:设计模式
(1)
main方法必须要static来修饰,是因为main方法是Java解释器调用的,那时候还没有任何对象产生。
用static修饰的方法,无须产生类的实例对象就可以调用该方法。
没有static修饰的方法,需要产生一个类的实例对象才可以调用该方法。

(2)
java main函数通过new一个对象调用非静态函数:
main方法都是静态方法,如果想调用其它的方法,要么只能是其它的静态方法,要么就是将当前类实例化在调用它的非静态方法 main方法里面调用非静态方法时,需要new一个对象,因为静态方法、静态类属于模板,非静态类和方法属于对象。

(3)
别的类里的非静态变量不可直接调用,也是需要在本类创建该变量(通过new的方法调用非静态函数创建)。

我自己设计的框架:
在这里插入图片描述


导师给我提供的更为规范 实际开发应用的框架:

在这里插入图片描述

框架梳理:

接口interface

IDevice:定义connect()判断函数、读写泛型函数
IEncoder:编写 读/写指令
getRead/WriteCommand
IDecoder:获取报文id和长度和值、检查合法性
getId、getHeadLength、getContentLength、getValue、checkIdLegality


通用/公共类

BytesOperate类:
负责合并两个byte数组(报文头+报文正文内容)

Converter类:
重写getBytes():将不同类型的数据转换成byte型
getShort、getChar、getInt、getLong、getFloat、getDouble、getString、getHexString、getCRC():将byte[]转换成不同类型的数据

enum PLCAddressType:
线圈寄存器、离散寄存器、保持寄存器、输入寄存器

OperateResult:泛型,存放操作结果判断等等

DeviceTemplate类:
实现接口implements IDevice 设置地址和端口:

public DeviceTemplate(String ipAddress, int port) {
    this.ipAddress = ipAddress;
    this.port = port;
}

判断是否连接成功:

public boolean connect() {
    try {
        if(!socket.isConnected() || socket.isClosed()){
            socket = new Socket();
            SocketAddress remoteAddr=new  / InetSocketAddress(ipAddress,port);
            socket.connect(remoteAddr, 3000);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return socket.isConnected() && !socket.isClosed();
}

发送数据:

private OperateResult<byte[]> Send(byte[] sendBuffer){
OperateResult<byte[]> result = new OperateResult<byte[]>();
    if(socket.isConnected()){
Pass
}

先展示要发送的数据,建立输出输入流:

System.out.println("Send:" +Converter.getHexString(sendBuffer));
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sendBuffer);
outputStream.flush();
InputStream is = socket.getInputStream();

接收时要先做判断:

byte[] headerBuffer = new byte[decoder.getHeadLength()];
if(Receive(is, headerBuffer, 3000) == headerBuffer.length){
    byte[] contentBuffer = new byte[decoder.getContentLength(headerBuffer)];
    if(Receive(is, contentBuffer, 3000) == contentBuffer.length) {
        byte[] recvBuffer = BytesOperate.addBytes(headerBuffer, contentBuffer);
        if(decoder.checkIdLegality(sendBuffer, recvBuffer))
        {
            if(decoder.checkPacketLegality(sendBuffer, recvBuffer)){
                if(decoder.checkSumLegality(sendBuffer, recvBuffer)){
                    System.out.println("Recv:" +Converter.getHexString(recvBuffer));
                    if(recvBuffer.length >= 9){
                        if(((recvBuffer[7] & 0X83) == 0X83) && ((recvBuffer[8] & 0X02) == 0X02)){
                            result.isSuccess = false;
                            result.message = "Send command error";
                        }else{
                            result.isSuccess = true;
                            result.message = "Success";
                            result.content = recvBuffer;
                        }

当缓冲区1数据长度报文头部长度(从decoder.getHeadLength获取)时->当缓冲区2数据长度报文正文长度(从decoder.getContentLength获取)时->合并报文->检查id合法,否则报错:

public boolean checkIdLegality(byte[] sendByte, byte[] recvByte) {
    if(sendByte.length > 2 && recvByte.length >2){
        return sendByte[0] == recvByte[0] && sendByte[1] == recvByte[1];
    }
    return false;
}

->输出byte转十六进制:判断数据字节长度&判断是否错误代码

接收函数:
计算并设置超时时间+接收数据的长度判断
比较长度大小 先读进缓冲区

private int Receive(InputStream is, byte[] recvBuffer, int
timeoutMillis) throws IOException {
    int bufferOffset = 0;
    long maxTimeMillis = System.currentTimeMillis() + timeoutMillis;
    while (System.currentTimeMillis() < maxTimeMillis && bufferOffset < recvBuffer.length) {
        int readLength = java.lang.Math.min(is.available(), recvBuffer.length - bufferOffset);
        int readResult = is.read(recvBuffer, bufferOffset, readLength);
        if (readResult == -1) {
            break;
        }
        bufferOffset += readResult;
    }
    return bufferOffset;
}

read(byte[] b, int off, int len):从b[off]开始将输入流中最多len个数据字节读入 byte 数组,以整数形式返回实际读取的字节数。

读函数:
泛型【在ModbusTcpDeviceTest被调用】
先判断是否连接成功:if(this.connect())
发送编码后的报文byte[]:Send(encoder.getReadCommand())
【定义在ModbusTcpEncoder】
根据响应报文判断是否发送成功:this.decoder.getValue()【定义在ModbusTcpDecoder】
否则报错

public <T> OperateResult<String> read(short dataAddress,
PLCAddressType addressType, short dataAddressLength, Class<T> clazz) {
        OperateResult<String> result = new OperateResult<String>();
        if(this.connect())
        {
            OperateResult<byte[]> operateResult = Send(encoder.getReadCommand(stationNumber, dataAddress, addressType, dataAddressLength));
            if(operateResult.isSuccess){
                result.isSuccess = true;
                result.content = this.decoder.getValue(operateResult.content, clazz);
                result.message = "Success";
            }else{
                result.isSuccess = false;
                result.message = operateResult.message;
            }
        }else{
            result.isSuccess = false;
            result.message = "Connect error";
        }
        return result;
    }
}

TCP类:

【要实现接口中定义的全部函数功能】

ModbusTcpDecoder implements Idecoder

从头指令获取报文正文长度:

public short getContentLength(byte[] recvHeadByte) {
    byte[] contentBytes = new byte[] {
        recvHeadByte[5], recvHeadByte[4]
    } ;
    short value = Converter.getShort(contentBytes);
    return Converter.getShort(contentBytes);
}

getValue()判断是短整型还是浮点型:取出相应的字节

public <T> String getValue(byte[] recvByte, Class<T> clazz){
    String result = null;
    try {
        switch (clazz.getName()){
            case "float": {
                byte[] dataBytes = new byte[]{ recvByte[11], recvByte[12], recvByte[9], recvByte[10]};
                float value = Converter.getFloat(dataBytes);
                result = String.valueOf(value);
                break;
            } case "short":{
                byte[] dataBytes = new byte[]{recvByte[9], recvByte[10]};
                short value = Converter.getShort(dataBytes);
                result = String.valueOf(value);
                break;
            }
            default:break;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;
}

ModbusTcpDevice extends DeviceTemplate 定义

public ModbusTcpDevice(String ipAddress, int port) {
    super(ipAddress, port);
    this.decoder = new ModbusTcpDecoder();
    this.encoder = new ModbusTcpEncoder();
}

ModbusTcpEncoder implements Iencoder 编码发送内容
读指令:

private short id = 0X01;
public byte[] getReadCommand(byte stationNumber, short dataAddress, PLCAddressType addressType, int dataAddressLength) {
    byte[] headerBuffer = Converter.getBytes((short)id++);
    byte[] dataAddressBuffer = Converter.getBytes(dataAddress);
    byte addressByte = (byte)(addressType.ordinal() + 1);
    return new byte[]{
            headerBuffer[0],headerBuffer[1],
            0X00,0X00,0X00,0X06,
            stationNumber,
            addressByte,
            dataAddressBuffer[1], dataAddressBuffer[0],
            0X00, (byte)dataAddressLength
    };
}

写指令可以参考开头提到的那篇rtu文章

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值