Quellcode durchsuchen

1、添加交互的测试报文解析、组帧功能

liweimin vor 4 Wochen
Ursprung
Commit
5bc7773da4

+ 6 - 0
ruoyi-device/pom.xml

@@ -38,6 +38,12 @@
             <groupId>io.netty</groupId>
             <artifactId>netty-all</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 3 - 8
ruoyi-device/src/main/java/com/ruoyi/device/mqtt/handler/encoder/AbstractEncoder.java

@@ -150,21 +150,16 @@ public abstract class AbstractEncoder<T> implements IEncoder<T>
     {
         int len = (hex.length() / 2);
         byte[] result = new byte[len];
-        char[] achar = hex.toCharArray();
         for (int i = 0; i < len; i++)
         {
             int pos = i * 2;
-            result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1]));
+            int high = Character.digit(hex.charAt(pos), 16);
+            int low = Character.digit(hex.charAt(pos + 1), 16);
+            result[i] = (byte) ((high << 4) | low);
         }
         return result;
     }
 
-    private int toByte(char c)
-    {
-        byte b = (byte) "0123456789ABCDEF".indexOf(c);
-        return b;
-    }
-
     /**
      * 生成topic
      *

+ 248 - 0
ruoyi-device/src/test/java/com/ruoyi/device/EncoderDecoderTest.java

@@ -0,0 +1,248 @@
+package com.ruoyi.device;
+
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.device.mqtt.constants.MqttConstants;
+import com.ruoyi.device.mqtt.domain.decoder.DeviceLoginRequest;
+import com.ruoyi.device.mqtt.domain.decoder.DevicePtRequest;
+import com.ruoyi.device.mqtt.enums.CmdTypeEnum;
+import com.ruoyi.device.mqtt.enums.VersionEnum;
+import com.ruoyi.device.mqtt.util.CRC16Standard;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.apache.commons.lang3.ArrayUtils;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * JSON 报文编解码示例:encoder 上行组帧,decoder 下行组帧(帧格式一致,均含 CRC16)
+ *
+ * @author lwm
+ */
+public class EncoderDecoderTest
+{
+
+    public void completeMessageStructure()
+    {
+        // 完整的报文结构说明
+        System.out.println("完整报文结构:");
+        System.out.println("┌─────────────────────────────────────┐");
+        System.out.println("│ Header (12字节)                      │");
+        System.out.println("│  - Frame Head: 0xFEFE (2字节)        │");
+        System.out.println("│  - Version: 协议版本 (1字节)          │");
+        System.out.println("│  - Msg ID: 消息ID (4字节)             │");
+        System.out.println("│  - Msg Type: 消息类型 (3字节)         │");
+        System.out.println("│  - Msg Length: JSON数据长度+2 (2字节) │");
+        System.out.println("├─────────────────────────────────────┤");
+        System.out.println("│ Body                                 │");
+        System.out.println("│  - JSON Data: JSON字符串 (N字节)      │");
+        System.out.println("├─────────────────────────────────────┤");
+        System.out.println("│ CRC16: 校验码 (2字节)                 │");
+        System.out.println("└─────────────────────────────────────┘");
+        System.out.println();
+    }
+
+    @Test
+    public void encoder()
+    {
+        completeMessageStructure();
+        String jsonString = JSON.toJSONString(buildDevicePtRequest());
+        System.out.println("【上行组帧】JSON序列化结果:");
+        System.out.println(jsonString);
+        // 模拟JSON字节数组构建(与AbstractJsonEncoder逻辑一致)
+        byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8);
+        byte[] bytes = encode(jsonBytes, 0x4A, 0x0000);
+        System.out.println("【上行】发送JSON消息完整报文:" + bytes2hex(bytes));
+        System.out.println("【上行】发送JSON消息完整报文长度:" + bytes.length);
+    }
+
+    public DeviceLoginRequest buildDeviceLoginRequest()
+    {
+        // 登录菜单权限请求
+        // topic: cpyypt/tsbup/9102/0000000005
+        DeviceLoginRequest request = new DeviceLoginRequest();
+        request.setCmdType(CmdTypeEnum.TSB_LOGIN.getCmdUpType());
+        request.setDeviceType("9102");
+        request.setDeviceSn(5L);
+        request.setUserName("wbjw");
+        request.setUserPassword("123456");
+        request.setImei("864793080085603");
+        return request;
+    }
+
+    public DevicePtRequest buildDevicePtRequest()
+    {
+        // 产测数据请求
+        // topic: cpyypt/tsbup/9102/864793080085603
+        DevicePtRequest request = new DevicePtRequest();
+        request.setCmdType(CmdTypeEnum.TSB_PT.getCmdUpType());
+        request.setDeviceType("9102");
+        request.setImei("864793080085603");
+        return request;
+    }
+
+    @Test
+    public void decoder()
+    {
+        completeMessageStructure();
+        System.out.println("【下行解析】完整报文:" + getDeviceLoginResponse());
+        byte[] bytes = hexStringToByte(getDeviceLoginResponse().replace(" ", ""));
+        String decode = decode(bytes);
+        System.out.println("【下行解析】报文体:" + decode);
+    }
+
+    public String getDeviceLoginResponse()
+    {
+        // {"cmdType":"tsb:login:down","code":1,"deviceSn":5,"deviceType":"9102","msg":"登录成功","nickName":"微宝金网","permissions":["tsb:device:add","tsb:device:query","tsb:device:list","tsb:device:edit","tsb:device:export","tsb:device:remove"],"phoneNumber":"15888888881","roleName":"普通角色","userName":"wbjw"}
+        return "fefe 0301 0000 004a 0000 3f01 7b22 636d 6454 7970 6522 3a22 7473 623a 6c6f 6769 6e3a 646f 776e 222c 2263 6f64 6522 3a31 2c22 6465 7669 6365 536e 223a 352c 2264 6576 6963 6554 7970 6522 3a22 3931 3032 222c 226d 7367 223a 22e7 99bb e5bd 95e6 8890 e58a 9f22 2c22 6e69 636b 4e61 6d65 223a 22e5 beae e5ae 9de9 8791 e7bd 9122 2c22 7065 726d 6973 7369 6f6e 7322 3a5b 2274 7362 3a64 6576 6963 653a 6164 6422 2c22 7473 623a 6465 7669 6365 3a71 7565 7279 222c 2274 7362 3a64 6576 6963 653a 6c69 7374 222c 2274 7362 3a64 6576 6963 653a 6564 6974 222c 2274 7362 3a64 6576 6963 653a 6578 706f 7274 222c 2274 7362 3a64 6576 6963 653a 7265 6d6f 7665 225d 2c22 7068 6f6e 654e 756d 6265 7222 3a22 3135 3838 3838 3838 3838 3122 2c22 726f 6c65 4e61 6d65 223a 22e6 99ae e980 9ae8 a792 e889 b222 2c22 7573 6572 4e61 6d65 223a 2277 626a 7722 7d00 bf";
+    }
+
+    public String getDevicePtResponse()
+    {
+        // {"cmdType":"tsb:pt:down","code":1,"deviceConfig":{"deviceProduceDate":"20260513","deviceSn":5,"deviceType":"9102"},"deviceSn":864793080085603,"deviceType":"9102","imei":"864793080085603","msg":"操作成功"}
+        return "fefe 0301 0000 004a 0000 d200 7b22 636d 6454 7970 6522 3a22 7473 623a 7074 3a64 6f77 6e22 2c22 636f 6465 223a 312c 2264 6576 6963 6543 6f6e 6669 6722 3a7b 2264 6576 6963 6550 726f 6475 6365 4461 7465 223a 2232 3032 3630 3531 3322 2c22 6465 7669 6365 536e 223a 352c 2264 6576 6963 6554 7970 6522 3a22 3931 3032 227d 2c22 6465 7669 6365 536e 223a 3836 3437 3933 3038 3030 3835 3630 332c 2264 6576 6963 6554 7970 6522 3a22 3931 3032 222c 2269 6d65 6922 3a22 3836 3437 3933 3038 3030 3835 3630 3322 2c22 6d73 6722 3a22 e693 8de4 bd9c e688 90e5 8a9f 227d 22f2";
+    }
+
+
+    /**
+     * 上行组帧测试(设备 → 平台)
+     * 12 字节头 + JSON Body + CRC16
+     */
+    public byte[] encode(byte[] jsonBytes, int firstType, int secondType)
+    {
+        ByteBuf header = Unpooled.buffer(800);
+        header.writeShortLE(MqttConstants.FRAME_HEADER);
+        header.writeByte(VersionEnum.THIRD.getValue());
+        header.writeIntLE(1);//Msg_ID
+        header.writeByte(firstType);
+        header.writeShortLE(secondType);
+
+        // 请求包体内容解析 - 使用JSON序列化
+        ByteBuf body = Unpooled.buffer(800);
+        body.writeBytes(jsonBytes);
+
+        // 获取可读字节数
+        int length = body.readableBytes();
+        // 分配一个新的数组来保存具有该长度的字节数据
+        byte[] msgBytes = new byte[length];
+        // 将字节复制到该数组
+        body.getBytes(body.readerIndex(), msgBytes);
+
+        // Msg_Len 消息长度,指示后面数据的长度,4字节的倍数 加上校验位2字节
+        header.writeShortLE(msgBytes.length + 2);
+        // 请求包内容
+        header.writeBytes(msgBytes);
+
+        int totalLength = header.readableBytes();
+        byte[] totalBytes = new byte[totalLength];
+        // 将字节复制到该数组
+        header.getBytes(header.readerIndex(), totalBytes);
+        header.release();
+        body.release();
+
+        byte[] crcBytes = CRC16Standard.getCRCBytes(totalBytes);
+        return ArrayUtils.addAll(totalBytes, crcBytes);
+    }
+
+    /**
+     * 下行解析测试(平台 → 设备)
+     * 将完整报文解析并反序列化为 Java 对象
+     */
+    public String decode(byte[] bytes)
+    {
+        System.out.println("\n========== 报文各部分详细解析 ==========");
+
+        // 1. 打印帧头 (2字节)
+        byte[] frameHeader = new byte[]{bytes[0], bytes[1]};
+        System.out.println("1. 帧头 (Frame Header): " + bytes2hex(frameHeader) + " -> 0xFEFE");
+
+        // 2. 打印协议版本 (1字节)
+        byte[] version = new byte[]{bytes[2]};
+        System.out.println("2. 协议版本 (Version): " + bytes2hex(version) + " -> " + (version[0] & 0xFF));
+
+        // 3. 打印消息ID (4字节)
+        byte[] msgId = new byte[]{bytes[3], bytes[4], bytes[5], bytes[6]};
+        System.out.println("3. 消息ID (Msg ID): " + bytes2hex(msgId));
+
+        // 4. 打印消息类型 - 第一分类 (1字节)
+        byte[] msgType1 = new byte[]{bytes[7]};
+        System.out.println("4. 消息类型-第一分类 (Msg Type 1): " + bytes2hex(msgType1) + " -> 0x" + String.format("%02X", msgType1[0] & 0xFF));
+
+        // 5. 打印消息类型 - 第二分类 (2字节)
+        byte[] msgType2 = new byte[]{bytes[8], bytes[9]};
+        System.out.println("5. 消息类型-第二分类 (Msg Type 2): " + bytes2hex(msgType2) + " -> 0x" + String.format("%02X", msgType2[1] & 0xFF) + String.format("%02X", msgType2[0] & 0xFF));
+
+        // 6. 打印消息长度 (2字节)
+        byte[] msgLength = new byte[]{bytes[10], bytes[11]};
+        int lengthValue = ((msgLength[1] & 0xFF) << 8) | (msgLength[0] & 0xFF);
+        System.out.println("6. 消息长度 (Msg Length) JSON数据长度+2: " + bytes2hex(msgLength) + " -> " + lengthValue + " 字节");
+
+        // 7. 打印Body部分 (JSON数据)
+        // Body从索引12开始,长度为lengthValue-2(减去CRC的2字节)
+        int bodyLength = lengthValue - 2;
+        byte[] bodyBytes = new byte[bodyLength];
+        System.arraycopy(bytes, 12, bodyBytes, 0, bodyLength);
+        System.out.println("7. Body部分 (JSON数据):");
+        System.out.println("   - Body长度: " + bodyLength + " 字节");
+        System.out.println("   - Body十六进制: " + bytes2hex(bodyBytes));
+
+        // 将Body还原为JSON字符串
+        String bodyJson = new String(bodyBytes, StandardCharsets.UTF_8);
+        System.out.println("   - Body还原为JSON: " + bodyJson);
+
+        // 8. 打印CRC16校验码 (2字节)
+        int crcStartIndex = 12 + bodyLength;
+        byte[] crcBytesPrint = new byte[]{bytes[crcStartIndex], bytes[crcStartIndex + 1]};
+        System.out.println("8. CRC16校验码 (CRC): " + bytes2hex(crcBytesPrint));
+        System.out.println("==========================================\n");
+        return bodyJson;
+    }
+
+    /**
+     * bytes数组 转换 十六进制字符串
+     *
+     * @param bytes 字节数组
+     * @return 十六进制字符串
+     */
+    public String bytes2hex(byte[] bytes)
+    {
+        if (bytes == null || bytes.length == 0)
+        {
+            return "[]";
+        }
+        StringBuilder sb = new StringBuilder("[");
+        for (byte b : bytes)
+        {
+            String tmp = Integer.toHexString(0xFF & b);
+            if (tmp.length() == 1)
+            {
+                tmp = "0" + tmp;
+            }
+            sb.append(tmp).append(' ');
+        }
+        sb.deleteCharAt(sb.length() - 1);
+        sb.append(']');
+        return sb.toString();
+    }
+
+    /**
+     * 十六进制字符串 转换 bytes数组
+     *
+     * @param hex 十六进制字符串
+     * @return bytes数组
+     */
+    private byte[] hexStringToByte(String hex)
+    {
+        int len = (hex.length() / 2);
+        byte[] result = new byte[len];
+        for (int i = 0; i < len; i++)
+        {
+            int pos = i * 2;
+            int high = Character.digit(hex.charAt(pos), 16);
+            int low = Character.digit(hex.charAt(pos + 1), 16);
+            result[i] = (byte) ((high << 4) | low);
+        }
+        return result;
+    }
+
+}