Ver código fonte

feat(services): 新增音频、雷达和水位服务模块

- 创建 audio-service 模块及其 Maven 配置文件
- 创建 radar-service 模块及其 Maven 配置文件
- 创建 water-level-service 模块及其 Maven 配置文件
- 为各服务模块添加 Spring Boot、MyBatis Plus 等必要依赖
- 实现音频服务中的协议工具类 ProtocolUtils
- 实现雷达服务中的协议工具类 ProtocolUtils
- 添加协议解析、帧构建和校验相关功能
- 集成 Nacos 服务注册发现和配置管理
- 添加 Sentinel 流量控制和 Netty 网络编程支持
林仔 1 mês atrás
pai
commit
38bd958125

+ 91 - 0
audio-service/pom.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.zksy</groupId>
+        <artifactId>pipe-ner-server</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <groupId>com.zksy</groupId>
+    <artifactId>audio-service</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <!--common-->
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <!--数据库-->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <!--mybatis-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <!--nacos 服务注册发现-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+        <!--负载均衡-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!--统一配置管理-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+        <!--加载bootstrap-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+        <!--sentinel-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+        </dependency>
+        <!--netty依赖-->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-math3</artifactId>
+            <version>3.6.1</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 361 - 0
audio-service/src/main/java/com/zksy/audio/utils/ProtocolUtils.java

@@ -0,0 +1,361 @@
+package com.zksy.audio.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+public class ProtocolUtils {
+    private static final Logger logger = Logger.getLogger(ProtocolUtils.class.getName());
+    /**
+     * 将2000年起始的时间戳转换为LocalDateTime
+     */
+    static LocalDateTime convertToDateTime(int timestamp) {
+        try {
+            // 定义基准时间:2000年1月1日 00:00:00
+            LocalDateTime baseTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+            // 累加时间戳(秒数)
+            return baseTime.plusSeconds(timestamp);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    // 获取当前时间并转换为2000年起始的时间戳
+    static int getCurrentTimestampSince2000() {
+        LocalDateTime now = LocalDateTime.now(ZoneOffset.of("+8"));
+        LocalDateTime baseTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+        // 计算时间差(秒数)
+        return (int) java.time.Duration.between(baseTime, now).getSeconds();
+    }
+    public static int calculateCRC16(byte[] data) {
+        int crc = 0xFFFF;
+        for (byte b : data) {
+            crc ^= (b & 0xFF);
+            for (int i = 0; i < 8; i++) {
+                if ((crc & 0x0001) != 0) crc = (crc >> 1) ^ 0xA001;
+                else crc >>= 1;
+            }
+        }
+        int high = (crc & 0xFF00) >> 8;
+        int low = crc & 0x00FF;
+        return (high << 8) | low;
+    }
+
+    public static byte calculateXorCheck(byte[] data) {
+        byte check = 0x00;
+        for (byte b : data) check ^= b;
+        return check;
+    }
+
+    /**
+     * 构建上报回应包(对应帧类型 0x31)
+     * 结构:
+     * [系统识别码(3)] [整帧长度(2)] [包序号(1)] [帧类型0x31(1)]
+     * [源地址长度(1)] [源地址(n)] [目的地址长度(1)] [目的地址(m)] [帧校验(1)]
+     *
+     * 示例(文档第11页):
+     * 12 34 56 00 16 81 31 0B 00 00 00 00 00 10 0B 13 81 23 45 67 80 D5
+     */
+    public static byte[] buildReportAckPacket(byte[] originalFrame, byte packetSequence) {
+        int srcLenIndex = 7;
+        int srcLen = originalFrame[srcLenIndex] & 0xFF;
+        int srcByteLen = (srcLen + 1) / 2;
+        int srcStart = srcLenIndex + 1;
+        int srcEnd = srcStart + srcByteLen;
+
+        int dstLenIndex = srcEnd;
+        int dstLen = originalFrame[dstLenIndex] & 0xFF;
+        int dstByteLen = (dstLen + 1) / 2;
+        int dstStart = dstLenIndex + 1;
+        int dstEnd = dstStart + dstByteLen;
+
+        byte[] response = new byte[3 + 2 + 1 + 1 + 1 + dstByteLen + 1 + srcByteLen + 1];
+        int idx = 0;
+
+        // 系统识别码 12 34 56
+        response[idx++] = 0x12;
+        response[idx++] = 0x34;
+        response[idx++] = 0x56;
+
+        // 长度(待填充)
+        int totalLen = response.length;
+        response[idx++] = (byte) ((totalLen >> 8) & 0xFF);
+        response[idx++] = (byte) (totalLen & 0xFF);
+
+        // 包序号(与原帧一致)
+        response[idx++] = packetSequence;
+
+        // 帧类型:上报回应
+        response[idx++] = 0x31;
+
+        // 源地址 = 原帧的目的地址
+        response[idx++] = (byte) dstLen;
+        System.arraycopy(originalFrame, dstStart, response, idx, dstByteLen);
+        idx += dstByteLen;
+
+        // 目的地址 = 原帧的源地址
+        response[idx++] = (byte) srcLen;
+        System.arraycopy(originalFrame, srcStart, response, idx, srcByteLen);
+        idx += srcByteLen;
+
+        // 异或校验(从系统识别码到帧内容)
+        byte xor = calculateXorCheck(Arrays.copyOfRange(response, 0, idx));
+        response[idx++] = xor;
+
+        return response;
+    }
+
+    /**
+     * 构建结束通讯回应包(对应帧类型 0x34)
+     * 结构:
+     * [系统识别码(3)] [整帧长度(2)] [包序号(0x80)] [帧类型0x34(1)]
+     * [源地址长度(1)] [源地址(n)] [目的地址长度(1)] [目的地址(m)] [帧校验(1)]
+     *
+     * 示例(文档第12页):
+     * 12 34 56 00 16 80 34 0B 13 81 23 45 67 80 0B 00 00 00 00 00 10 D1
+     */
+    public static byte[] buildShutdownAckPacket(byte[] originalFrame) {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(originalFrame);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1];
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5];
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(originalFrame, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(originalFrame, destAddrStart, destAddrEnd);
+
+        // 步骤3:构造应答帧
+        int totalLen = 3 + 2 + 1 + 1 + 1 + destAddrByteLen + 1 + srcAddrByteLen + 1;
+        byte[] response = new byte[totalLen];
+        int idx = 0;
+
+        // 系统识别码
+        response[idx++] = 0x12;
+        response[idx++] = 0x34;
+        response[idx++] = 0x56;
+
+        // 长度
+        response[idx++] = (byte) ((totalLen >> 8) & 0xFF);
+        response[idx++] = (byte) (totalLen & 0xFF);
+
+        // 包序号0x80 + 帧类型0x34
+        response[idx++] = (byte) 0x80;
+        response[idx++] = 0x34;
+
+        // 源地址=原目的地址(动态)
+        response[idx++] = (byte) destLenFlag;
+        System.arraycopy(destAddr, 0, response, idx, destAddrByteLen);
+        idx += destAddrByteLen;
+
+        // 目的地址=原源地址(动态)
+        response[idx++] = (byte) srcLenFlag;
+        System.arraycopy(srcAddr, 0, response, idx, srcAddrByteLen);
+        idx += srcAddrByteLen;
+
+        // 异或校验
+        byte xor = calculateXorCheck(Arrays.copyOfRange(response, 0, idx));
+        response[idx++] = xor;
+
+        return response;
+    }
+    public static byte[] buildCustomReplyFrame(byte[] request) throws IOException {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(request);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1]; // 原源地址长度 m
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5]; // 原目的地址长度 n
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // 步骤3:固定字段提取
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 系统识别码(3字节)
+        byte packetSeq = request[5]; // 包序号(1字节)
+        byte frameType = request[6]; // 帧类型0x31(1字节)
+
+        // 帧长度字段值 = 系统识别码(3字节)+2(自身) + 1(包序号) + 1(帧类型) + 1(源地址标识) + n(源地址) + 1(目的地址标识) + m(目的地址)
+        int frameLenValue = 3 + 2 + 1 + 1 + 1 + destAddrByteLen + 1 + srcAddrByteLen+1;
+
+        // 步骤4:构造帧体(直接写入正确长度,无需占位回填)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId); // 系统识别码(3字节)
+        // 写入帧长度字段(包含自身2字节,高字节在前)
+        bos.write((byte) ((frameLenValue >> 8) & 0xFF));
+        bos.write((byte) (frameLenValue & 0xFF));
+
+        // 步骤5:地址交换(动态长度)
+        bos.write(packetSeq);          // 包序号
+        bos.write(frameType);          // 帧类型
+        bos.write(destLenFlag);        // 应答源地址标识=原目的标识
+        bos.write(destAddr);           // 应答源地址=原目的地址(n字节)
+        bos.write(srcLenFlag);         // 应答目的地址标识=原源标识
+        bos.write(srcAddr);            // 应答目的地址=原源地址(m字节)
+
+        // 步骤6:计算异或校验(仅计算不含校验位的帧体)
+        byte[] tempFrame = bos.toByteArray();
+        byte xor = calculateXorCheck(tempFrame);
+
+        // 步骤7:拼接最终帧(帧体 + 异或校验位)
+        ByteArrayOutputStream finalBos = new ByteArrayOutputStream();
+        finalBos.write(tempFrame);
+        finalBos.write(xor);
+
+        return finalBos.toByteArray();
+    }
+    public static byte[] buildEndReplyFrame(byte[] request) throws IOException {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(request);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1]; // 源地址长度 n
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5]; // 目的地址长度 m
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // 步骤3:固定字段提取
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 系统识别码(3字节)
+        byte packetSeq = request[5]; // 包序号(1字节)
+
+        // 步骤4:时间戳(4字节)
+        long secondsSince2000 = getSecondsSince2000();
+        byte[] timestamp = new byte[4];
+        timestamp[0] = (byte) ((secondsSince2000 >> 24) & 0xFF);
+        timestamp[1] = (byte) ((secondsSince2000 >> 16) & 0xFF);
+        timestamp[2] = (byte) ((secondsSince2000 >> 8) & 0xFF);
+        timestamp[3] = (byte) (secondsSince2000 & 0xFF);
+
+        // ========== 核心修正:计算包含自身2字节的帧长度字段值 ==========
+        // 帧长度字段值 = 2(自身) + 1(包序号) + 1(帧类型) + 1(源地址标识) + n(源地址) + 1(目的地址标识) + m(目的地址) + 4(时间戳)
+        int frameLenValue =3+ 2 + 1 + 1 + 1 + srcAddrByteLen + 1 + destAddrByteLen + 4+1;
+        // 拆分高低字节
+        byte lenHigh = (byte) ((frameLenValue >> 8) & 0xFF);
+        byte lenLow = (byte) (frameLenValue & 0xFF);
+
+        // 步骤5:构造内容区(包序号到时间戳,不含长度字段)
+        ByteArrayOutputStream content = new ByteArrayOutputStream();
+        content.write(0X80);  // 包序号(1字节)
+        content.write(0x34);       // 帧类型0x34(1字节)
+        content.write(destLenFlag);// 目的地址标识(1字节)
+        content.write(destAddr);   // 目的地址(m字节)
+        content.write(srcLenFlag); // 源地址标识(1字节)srcLenFlag
+        content.write(srcAddr);    // 源地址(n字节)
+        content.write(timestamp);  // 时间戳(4字节)
+        byte[] contentBytes = content.toByteArray();
+
+        // 步骤6:拼帧(系统识别码 + 长度字段 + 内容区)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId);      // 系统识别码(3字节)
+        bos.write(lenHigh);    // 长度高字节(包含自身2字节的总长度)
+        bos.write(lenLow);     // 长度低字节
+        bos.write(contentBytes);
+
+        // 步骤7:计算并添加异或校验(仅计算不含校验位的帧体)
+        byte[] noCheck = bos.toByteArray();
+        byte xor = calculateXorCheck(noCheck);
+        bos.write(xor);
+
+        byte[] result = bos.toByteArray();
+        System.out.println("结束帧应答: " + printHex(result));
+        return result;
+    }
+
+    private static long getSecondsSince2000() {
+        LocalDateTime base = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+        LocalDateTime now = LocalDateTime.now(ZoneOffset.of("+8"));
+        return Duration.between(base, now).getSeconds();
+    }
+    private static String printHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) sb.append(String.format("%02X ", b));
+        return sb.toString().trim();
+    }
+    /**
+     * 核心工具方法:根据地址标识计算地址字节长度
+     * 规则:标识值转10进制 → 奇数+1 → 除以2
+     */
+    private static int calculateAddrByteLength(int lenFlag) {
+        int temp = lenFlag;
+        // 奇数则+1
+        if (temp % 2 != 0) {
+            temp += 1;
+        }
+        // 除以2得到字节长度
+        return temp / 2;
+    }
+    /**
+     * 核心工具:正向解析源/目的地址信息(贴合实际报文结构)
+     * 逻辑:
+     * 1. 正向定位源地址标识(索引7)、源地址、目的地址标识、目的地址
+     * 2. 找 0x01 2C 验证地址段结束位置,确保解析合法
+     * @return 数组格式:[srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd, destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd]
+     */
+    private static int[] extractAddrInfo(byte[] request) {
+        if (request == null || request.length < 25) { // 适配实际报文最小长度(82字节)
+            throw new IllegalArgumentException("请求帧长度不足,无法解析地址信息,长度=" + (request == null ? 0 : request.length));
+        }
+
+        // ========== 步骤1:正向定位核心索引(贴合协议固定结构) ==========
+        int srcLenIndex = 7; // 源地址标识固定在索引7
+        int srcLenFlag = request[srcLenIndex] & 0xFF;
+        int srcAddrByteLen = calculateAddrByteLength(srcLenFlag);
+        int srcAddrStart = srcLenIndex + 1;
+        int srcAddrEnd = srcAddrStart + srcAddrByteLen;
+
+        // 目的地址标识 = 源地址结束索引
+        int destLenIndex = srcAddrEnd;
+        int destLenFlag = request[destLenIndex] & 0xFF;
+        int destAddrByteLen = calculateAddrByteLength(destLenFlag);
+        int destAddrStart = destLenIndex + 1;
+        int destAddrEnd = destAddrStart + destAddrByteLen;
+
+        // ========== 步骤2:验证地址段结束位置(找 0x01 2C) ==========
+        int deviceCodeIndex = destAddrEnd;
+        // 校验设备编码+功能码是否为 0x01 2C(地址段后必须紧跟)
+        if (deviceCodeIndex + 1 >= request.length
+                || request[deviceCodeIndex] != 0x01
+                || request[deviceCodeIndex + 1] != 0x2C) {
+            throw new IllegalArgumentException("地址段结束后未找到 0x01 2C 序列,地址解析异常:设备编码索引=" + deviceCodeIndex);
+        }
+
+        // ========== 步骤3:范围最终校验 ==========
+        // 源地址范围校验
+        if (srcAddrStart < 8 || srcAddrEnd > destLenIndex) {
+            throw new IllegalArgumentException(
+                    String.format("源地址范围异常:标识索引=%d 标识值=%d 计算长度=%d 起始=%d 结束=%d",
+                            srcLenIndex, srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd));
+        }
+        // 目的地址范围校验
+        if (destAddrStart < (destLenIndex + 1) || destAddrEnd > deviceCodeIndex) {
+            throw new IllegalArgumentException(
+                    String.format("目的地址范围异常:标识索引=%d 标识值=%d 计算长度=%d 起始=%d 结束=%d",
+                            destLenIndex, destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd));
+        }
+
+        return new int[]{
+                srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd,
+                destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd
+        };
+    }
+
+}

+ 16 - 0
environment-service/src/main/java/com/zksy/environment/config/RestTemplateConfig.java

@@ -0,0 +1,16 @@
+package com.zksy.environment.config;
+
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    @LoadBalanced
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 91 - 0
radar-service/pom.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.zksy</groupId>
+        <artifactId>pipe-ner-server</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <groupId>com.zksy</groupId>
+    <artifactId>radar-service</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <!--common-->
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-api-service</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zksy-system</artifactId>
+            <version>3.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <!--数据库-->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <!--mybatis-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <!--nacos 服务注册发现-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+        <!--负载均衡-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!--统一配置管理-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+        <!--加载bootstrap-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+        <!--sentinel-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+        </dependency>
+        <!--netty依赖-->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 361 - 0
radar-service/src/main/java/com/zksy/radar/utils/ProtocolUtils.java

@@ -0,0 +1,361 @@
+package com.zksy.radar.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+public class ProtocolUtils {
+    private static final Logger logger = Logger.getLogger(ProtocolUtils.class.getName());
+    /**
+     * 将2000年起始的时间戳转换为LocalDateTime
+     */
+    static LocalDateTime convertToDateTime(int timestamp) {
+        try {
+            // 定义基准时间:2000年1月1日 00:00:00
+            LocalDateTime baseTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+            // 累加时间戳(秒数)
+            return baseTime.plusSeconds(timestamp);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    // 获取当前时间并转换为2000年起始的时间戳
+    static int getCurrentTimestampSince2000() {
+        LocalDateTime now = LocalDateTime.now(ZoneOffset.of("+8"));
+        LocalDateTime baseTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+        // 计算时间差(秒数)
+        return (int) java.time.Duration.between(baseTime, now).getSeconds();
+    }
+    public static int calculateCRC16(byte[] data) {
+        int crc = 0xFFFF;
+        for (byte b : data) {
+            crc ^= (b & 0xFF);
+            for (int i = 0; i < 8; i++) {
+                if ((crc & 0x0001) != 0) crc = (crc >> 1) ^ 0xA001;
+                else crc >>= 1;
+            }
+        }
+        int high = (crc & 0xFF00) >> 8;
+        int low = crc & 0x00FF;
+        return (high << 8) | low;
+    }
+
+    public static byte calculateXorCheck(byte[] data) {
+        byte check = 0x00;
+        for (byte b : data) check ^= b;
+        return check;
+    }
+
+    /**
+     * 构建上报回应包(对应帧类型 0x31)
+     * 结构:
+     * [系统识别码(3)] [整帧长度(2)] [包序号(1)] [帧类型0x31(1)]
+     * [源地址长度(1)] [源地址(n)] [目的地址长度(1)] [目的地址(m)] [帧校验(1)]
+     *
+     * 示例(文档第11页):
+     * 12 34 56 00 16 81 31 0B 00 00 00 00 00 10 0B 13 81 23 45 67 80 D5
+     */
+    public static byte[] buildReportAckPacket(byte[] originalFrame, byte packetSequence) {
+        int srcLenIndex = 7;
+        int srcLen = originalFrame[srcLenIndex] & 0xFF;
+        int srcByteLen = (srcLen + 1) / 2;
+        int srcStart = srcLenIndex + 1;
+        int srcEnd = srcStart + srcByteLen;
+
+        int dstLenIndex = srcEnd;
+        int dstLen = originalFrame[dstLenIndex] & 0xFF;
+        int dstByteLen = (dstLen + 1) / 2;
+        int dstStart = dstLenIndex + 1;
+        int dstEnd = dstStart + dstByteLen;
+
+        byte[] response = new byte[3 + 2 + 1 + 1 + 1 + dstByteLen + 1 + srcByteLen + 1];
+        int idx = 0;
+
+        // 系统识别码 12 34 56
+        response[idx++] = 0x12;
+        response[idx++] = 0x34;
+        response[idx++] = 0x56;
+
+        // 长度(待填充)
+        int totalLen = response.length;
+        response[idx++] = (byte) ((totalLen >> 8) & 0xFF);
+        response[idx++] = (byte) (totalLen & 0xFF);
+
+        // 包序号(与原帧一致)
+        response[idx++] = packetSequence;
+
+        // 帧类型:上报回应
+        response[idx++] = 0x31;
+
+        // 源地址 = 原帧的目的地址
+        response[idx++] = (byte) dstLen;
+        System.arraycopy(originalFrame, dstStart, response, idx, dstByteLen);
+        idx += dstByteLen;
+
+        // 目的地址 = 原帧的源地址
+        response[idx++] = (byte) srcLen;
+        System.arraycopy(originalFrame, srcStart, response, idx, srcByteLen);
+        idx += srcByteLen;
+
+        // 异或校验(从系统识别码到帧内容)
+        byte xor = calculateXorCheck(Arrays.copyOfRange(response, 0, idx));
+        response[idx++] = xor;
+
+        return response;
+    }
+
+    /**
+     * 构建结束通讯回应包(对应帧类型 0x34)
+     * 结构:
+     * [系统识别码(3)] [整帧长度(2)] [包序号(0x80)] [帧类型0x34(1)]
+     * [源地址长度(1)] [源地址(n)] [目的地址长度(1)] [目的地址(m)] [帧校验(1)]
+     *
+     * 示例(文档第12页):
+     * 12 34 56 00 16 80 34 0B 13 81 23 45 67 80 0B 00 00 00 00 00 10 D1
+     */
+    public static byte[] buildShutdownAckPacket(byte[] originalFrame) {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(originalFrame);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1];
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5];
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(originalFrame, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(originalFrame, destAddrStart, destAddrEnd);
+
+        // 步骤3:构造应答帧
+        int totalLen = 3 + 2 + 1 + 1 + 1 + destAddrByteLen + 1 + srcAddrByteLen + 1;
+        byte[] response = new byte[totalLen];
+        int idx = 0;
+
+        // 系统识别码
+        response[idx++] = 0x12;
+        response[idx++] = 0x34;
+        response[idx++] = 0x56;
+
+        // 长度
+        response[idx++] = (byte) ((totalLen >> 8) & 0xFF);
+        response[idx++] = (byte) (totalLen & 0xFF);
+
+        // 包序号0x80 + 帧类型0x34
+        response[idx++] = (byte) 0x80;
+        response[idx++] = 0x34;
+
+        // 源地址=原目的地址(动态)
+        response[idx++] = (byte) destLenFlag;
+        System.arraycopy(destAddr, 0, response, idx, destAddrByteLen);
+        idx += destAddrByteLen;
+
+        // 目的地址=原源地址(动态)
+        response[idx++] = (byte) srcLenFlag;
+        System.arraycopy(srcAddr, 0, response, idx, srcAddrByteLen);
+        idx += srcAddrByteLen;
+
+        // 异或校验
+        byte xor = calculateXorCheck(Arrays.copyOfRange(response, 0, idx));
+        response[idx++] = xor;
+
+        return response;
+    }
+    public static byte[] buildCustomReplyFrame(byte[] request) throws IOException {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(request);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1]; // 原源地址长度 m
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5]; // 原目的地址长度 n
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // 步骤3:固定字段提取
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 系统识别码(3字节)
+        byte packetSeq = request[5]; // 包序号(1字节)
+        byte frameType = request[6]; // 帧类型0x31(1字节)
+
+        // 帧长度字段值 = 系统识别码(3字节)+2(自身) + 1(包序号) + 1(帧类型) + 1(源地址标识) + n(源地址) + 1(目的地址标识) + m(目的地址)
+        int frameLenValue = 3 + 2 + 1 + 1 + 1 + destAddrByteLen + 1 + srcAddrByteLen+1;
+
+        // 步骤4:构造帧体(直接写入正确长度,无需占位回填)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId); // 系统识别码(3字节)
+        // 写入帧长度字段(包含自身2字节,高字节在前)
+        bos.write((byte) ((frameLenValue >> 8) & 0xFF));
+        bos.write((byte) (frameLenValue & 0xFF));
+
+        // 步骤5:地址交换(动态长度)
+        bos.write(packetSeq);          // 包序号
+        bos.write(frameType);          // 帧类型
+        bos.write(destLenFlag);        // 应答源地址标识=原目的标识
+        bos.write(destAddr);           // 应答源地址=原目的地址(n字节)
+        bos.write(srcLenFlag);         // 应答目的地址标识=原源标识
+        bos.write(srcAddr);            // 应答目的地址=原源地址(m字节)
+
+        // 步骤6:计算异或校验(仅计算不含校验位的帧体)
+        byte[] tempFrame = bos.toByteArray();
+        byte xor = calculateXorCheck(tempFrame);
+
+        // 步骤7:拼接最终帧(帧体 + 异或校验位)
+        ByteArrayOutputStream finalBos = new ByteArrayOutputStream();
+        finalBos.write(tempFrame);
+        finalBos.write(xor);
+
+        return finalBos.toByteArray();
+    }
+    public static byte[] buildEndReplyFrame(byte[] request) throws IOException {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(request);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1]; // 源地址长度 n
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5]; // 目的地址长度 m
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // 步骤3:固定字段提取
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 系统识别码(3字节)
+        byte packetSeq = request[5]; // 包序号(1字节)
+
+        // 步骤4:时间戳(4字节)
+        long secondsSince2000 = getSecondsSince2000();
+        byte[] timestamp = new byte[4];
+        timestamp[0] = (byte) ((secondsSince2000 >> 24) & 0xFF);
+        timestamp[1] = (byte) ((secondsSince2000 >> 16) & 0xFF);
+        timestamp[2] = (byte) ((secondsSince2000 >> 8) & 0xFF);
+        timestamp[3] = (byte) (secondsSince2000 & 0xFF);
+
+        // ========== 核心修正:计算包含自身2字节的帧长度字段值 ==========
+        // 帧长度字段值 = 2(自身) + 1(包序号) + 1(帧类型) + 1(源地址标识) + n(源地址) + 1(目的地址标识) + m(目的地址) + 4(时间戳)
+        int frameLenValue =3+ 2 + 1 + 1 + 1 + srcAddrByteLen + 1 + destAddrByteLen + 4+1;
+        // 拆分高低字节
+        byte lenHigh = (byte) ((frameLenValue >> 8) & 0xFF);
+        byte lenLow = (byte) (frameLenValue & 0xFF);
+
+        // 步骤5:构造内容区(包序号到时间戳,不含长度字段)
+        ByteArrayOutputStream content = new ByteArrayOutputStream();
+        content.write(0X80);  // 包序号(1字节)
+        content.write(0x34);       // 帧类型0x34(1字节)
+        content.write(destLenFlag);// 目的地址标识(1字节)
+        content.write(destAddr);   // 目的地址(m字节)
+        content.write(srcLenFlag); // 源地址标识(1字节)srcLenFlag
+        content.write(srcAddr);    // 源地址(n字节)
+        content.write(timestamp);  // 时间戳(4字节)
+        byte[] contentBytes = content.toByteArray();
+
+        // 步骤6:拼帧(系统识别码 + 长度字段 + 内容区)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId);      // 系统识别码(3字节)
+        bos.write(lenHigh);    // 长度高字节(包含自身2字节的总长度)
+        bos.write(lenLow);     // 长度低字节
+        bos.write(contentBytes);
+
+        // 步骤7:计算并添加异或校验(仅计算不含校验位的帧体)
+        byte[] noCheck = bos.toByteArray();
+        byte xor = calculateXorCheck(noCheck);
+        bos.write(xor);
+
+        byte[] result = bos.toByteArray();
+        System.out.println("结束帧应答: " + printHex(result));
+        return result;
+    }
+
+    private static long getSecondsSince2000() {
+        LocalDateTime base = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+        LocalDateTime now = LocalDateTime.now(ZoneOffset.of("+8"));
+        return Duration.between(base, now).getSeconds();
+    }
+    private static String printHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) sb.append(String.format("%02X ", b));
+        return sb.toString().trim();
+    }
+    /**
+     * 核心工具方法:根据地址标识计算地址字节长度
+     * 规则:标识值转10进制 → 奇数+1 → 除以2
+     */
+    private static int calculateAddrByteLength(int lenFlag) {
+        int temp = lenFlag;
+        // 奇数则+1
+        if (temp % 2 != 0) {
+            temp += 1;
+        }
+        // 除以2得到字节长度
+        return temp / 2;
+    }
+    /**
+     * 核心工具:正向解析源/目的地址信息(贴合实际报文结构)
+     * 逻辑:
+     * 1. 正向定位源地址标识(索引7)、源地址、目的地址标识、目的地址
+     * 2. 找 0x01 2C 验证地址段结束位置,确保解析合法
+     * @return 数组格式:[srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd, destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd]
+     */
+    private static int[] extractAddrInfo(byte[] request) {
+        if (request == null || request.length < 25) { // 适配实际报文最小长度(82字节)
+            throw new IllegalArgumentException("请求帧长度不足,无法解析地址信息,长度=" + (request == null ? 0 : request.length));
+        }
+
+        // ========== 步骤1:正向定位核心索引(贴合协议固定结构) ==========
+        int srcLenIndex = 7; // 源地址标识固定在索引7
+        int srcLenFlag = request[srcLenIndex] & 0xFF;
+        int srcAddrByteLen = calculateAddrByteLength(srcLenFlag);
+        int srcAddrStart = srcLenIndex + 1;
+        int srcAddrEnd = srcAddrStart + srcAddrByteLen;
+
+        // 目的地址标识 = 源地址结束索引
+        int destLenIndex = srcAddrEnd;
+        int destLenFlag = request[destLenIndex] & 0xFF;
+        int destAddrByteLen = calculateAddrByteLength(destLenFlag);
+        int destAddrStart = destLenIndex + 1;
+        int destAddrEnd = destAddrStart + destAddrByteLen;
+
+        // ========== 步骤2:验证地址段结束位置(找 0x01 2C) ==========
+        int deviceCodeIndex = destAddrEnd;
+        // 校验设备编码+功能码是否为 0x01 2C(地址段后必须紧跟)
+        if (deviceCodeIndex + 1 >= request.length
+                || request[deviceCodeIndex] != 0x01
+                || request[deviceCodeIndex + 1] != 0x2C) {
+            throw new IllegalArgumentException("地址段结束后未找到 0x01 2C 序列,地址解析异常:设备编码索引=" + deviceCodeIndex);
+        }
+
+        // ========== 步骤3:范围最终校验 ==========
+        // 源地址范围校验
+        if (srcAddrStart < 8 || srcAddrEnd > destLenIndex) {
+            throw new IllegalArgumentException(
+                    String.format("源地址范围异常:标识索引=%d 标识值=%d 计算长度=%d 起始=%d 结束=%d",
+                            srcLenIndex, srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd));
+        }
+        // 目的地址范围校验
+        if (destAddrStart < (destLenIndex + 1) || destAddrEnd > deviceCodeIndex) {
+            throw new IllegalArgumentException(
+                    String.format("目的地址范围异常:标识索引=%d 标识值=%d 计算长度=%d 起始=%d 结束=%d",
+                            destLenIndex, destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd));
+        }
+
+        return new int[]{
+                srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd,
+                destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd
+        };
+    }
+
+}

+ 91 - 0
water-level-service/pom.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.zksy</groupId>
+        <artifactId>pipe-ner-server</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <groupId>com.zksy</groupId>
+    <artifactId>water-level-service</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <!--common-->
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zksy-system</artifactId>
+            <version>3.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zksy</groupId>
+            <artifactId>zk-api-service</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <!--数据库-->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <!--mybatis-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <!--nacos 服务注册发现-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+        <!--负载均衡-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!--统一配置管理-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+        <!--加载bootstrap-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+        <!--sentinel-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+        </dependency>
+        <!--netty依赖-->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 361 - 0
water-level-service/src/main/java/com/zksy/water/utils/ProtocolUtils.java

@@ -0,0 +1,361 @@
+package com.zksy.water.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+public class ProtocolUtils {
+    private static final Logger logger = Logger.getLogger(ProtocolUtils.class.getName());
+    /**
+     * 将2000年起始的时间戳转换为LocalDateTime
+     */
+    static LocalDateTime convertToDateTime(int timestamp) {
+        try {
+            // 定义基准时间:2000年1月1日 00:00:00
+            LocalDateTime baseTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+            // 累加时间戳(秒数)
+            return baseTime.plusSeconds(timestamp);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    // 获取当前时间并转换为2000年起始的时间戳
+    static int getCurrentTimestampSince2000() {
+        LocalDateTime now = LocalDateTime.now(ZoneOffset.of("+8"));
+        LocalDateTime baseTime = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+        // 计算时间差(秒数)
+        return (int) java.time.Duration.between(baseTime, now).getSeconds();
+    }
+    public static int calculateCRC16(byte[] data) {
+        int crc = 0xFFFF;
+        for (byte b : data) {
+            crc ^= (b & 0xFF);
+            for (int i = 0; i < 8; i++) {
+                if ((crc & 0x0001) != 0) crc = (crc >> 1) ^ 0xA001;
+                else crc >>= 1;
+            }
+        }
+        int high = (crc & 0xFF00) >> 8;
+        int low = crc & 0x00FF;
+        return (high << 8) | low;
+    }
+
+    public static byte calculateXorCheck(byte[] data) {
+        byte check = 0x00;
+        for (byte b : data) check ^= b;
+        return check;
+    }
+
+    /**
+     * 构建上报回应包(对应帧类型 0x31)
+     * 结构:
+     * [系统识别码(3)] [整帧长度(2)] [包序号(1)] [帧类型0x31(1)]
+     * [源地址长度(1)] [源地址(n)] [目的地址长度(1)] [目的地址(m)] [帧校验(1)]
+     *
+     * 示例(文档第11页):
+     * 12 34 56 00 16 81 31 0B 00 00 00 00 00 10 0B 13 81 23 45 67 80 D5
+     */
+    public static byte[] buildReportAckPacket(byte[] originalFrame, byte packetSequence) {
+        int srcLenIndex = 7;
+        int srcLen = originalFrame[srcLenIndex] & 0xFF;
+        int srcByteLen = (srcLen + 1) / 2;
+        int srcStart = srcLenIndex + 1;
+        int srcEnd = srcStart + srcByteLen;
+
+        int dstLenIndex = srcEnd;
+        int dstLen = originalFrame[dstLenIndex] & 0xFF;
+        int dstByteLen = (dstLen + 1) / 2;
+        int dstStart = dstLenIndex + 1;
+        int dstEnd = dstStart + dstByteLen;
+
+        byte[] response = new byte[3 + 2 + 1 + 1 + 1 + dstByteLen + 1 + srcByteLen + 1];
+        int idx = 0;
+
+        // 系统识别码 12 34 56
+        response[idx++] = 0x12;
+        response[idx++] = 0x34;
+        response[idx++] = 0x56;
+
+        // 长度(待填充)
+        int totalLen = response.length;
+        response[idx++] = (byte) ((totalLen >> 8) & 0xFF);
+        response[idx++] = (byte) (totalLen & 0xFF);
+
+        // 包序号(与原帧一致)
+        response[idx++] = packetSequence;
+
+        // 帧类型:上报回应
+        response[idx++] = 0x31;
+
+        // 源地址 = 原帧的目的地址
+        response[idx++] = (byte) dstLen;
+        System.arraycopy(originalFrame, dstStart, response, idx, dstByteLen);
+        idx += dstByteLen;
+
+        // 目的地址 = 原帧的源地址
+        response[idx++] = (byte) srcLen;
+        System.arraycopy(originalFrame, srcStart, response, idx, srcByteLen);
+        idx += srcByteLen;
+
+        // 异或校验(从系统识别码到帧内容)
+        byte xor = calculateXorCheck(Arrays.copyOfRange(response, 0, idx));
+        response[idx++] = xor;
+
+        return response;
+    }
+
+    /**
+     * 构建结束通讯回应包(对应帧类型 0x34)
+     * 结构:
+     * [系统识别码(3)] [整帧长度(2)] [包序号(0x80)] [帧类型0x34(1)]
+     * [源地址长度(1)] [源地址(n)] [目的地址长度(1)] [目的地址(m)] [帧校验(1)]
+     *
+     * 示例(文档第12页):
+     * 12 34 56 00 16 80 34 0B 13 81 23 45 67 80 0B 00 00 00 00 00 10 D1
+     */
+    public static byte[] buildShutdownAckPacket(byte[] originalFrame) {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(originalFrame);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1];
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5];
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(originalFrame, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(originalFrame, destAddrStart, destAddrEnd);
+
+        // 步骤3:构造应答帧
+        int totalLen = 3 + 2 + 1 + 1 + 1 + destAddrByteLen + 1 + srcAddrByteLen + 1;
+        byte[] response = new byte[totalLen];
+        int idx = 0;
+
+        // 系统识别码
+        response[idx++] = 0x12;
+        response[idx++] = 0x34;
+        response[idx++] = 0x56;
+
+        // 长度
+        response[idx++] = (byte) ((totalLen >> 8) & 0xFF);
+        response[idx++] = (byte) (totalLen & 0xFF);
+
+        // 包序号0x80 + 帧类型0x34
+        response[idx++] = (byte) 0x80;
+        response[idx++] = 0x34;
+
+        // 源地址=原目的地址(动态)
+        response[idx++] = (byte) destLenFlag;
+        System.arraycopy(destAddr, 0, response, idx, destAddrByteLen);
+        idx += destAddrByteLen;
+
+        // 目的地址=原源地址(动态)
+        response[idx++] = (byte) srcLenFlag;
+        System.arraycopy(srcAddr, 0, response, idx, srcAddrByteLen);
+        idx += srcAddrByteLen;
+
+        // 异或校验
+        byte xor = calculateXorCheck(Arrays.copyOfRange(response, 0, idx));
+        response[idx++] = xor;
+
+        return response;
+    }
+    public static byte[] buildCustomReplyFrame(byte[] request) throws IOException {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(request);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1]; // 原源地址长度 m
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5]; // 原目的地址长度 n
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // 步骤3:固定字段提取
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 系统识别码(3字节)
+        byte packetSeq = request[5]; // 包序号(1字节)
+        byte frameType = request[6]; // 帧类型0x31(1字节)
+
+        // 帧长度字段值 = 系统识别码(3字节)+2(自身) + 1(包序号) + 1(帧类型) + 1(源地址标识) + n(源地址) + 1(目的地址标识) + m(目的地址)
+        int frameLenValue = 3 + 2 + 1 + 1 + 1 + destAddrByteLen + 1 + srcAddrByteLen+1;
+
+        // 步骤4:构造帧体(直接写入正确长度,无需占位回填)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId); // 系统识别码(3字节)
+        // 写入帧长度字段(包含自身2字节,高字节在前)
+        bos.write((byte) ((frameLenValue >> 8) & 0xFF));
+        bos.write((byte) (frameLenValue & 0xFF));
+
+        // 步骤5:地址交换(动态长度)
+        bos.write(packetSeq);          // 包序号
+        bos.write(frameType);          // 帧类型
+        bos.write(destLenFlag);        // 应答源地址标识=原目的标识
+        bos.write(destAddr);           // 应答源地址=原目的地址(n字节)
+        bos.write(srcLenFlag);         // 应答目的地址标识=原源标识
+        bos.write(srcAddr);            // 应答目的地址=原源地址(m字节)
+
+        // 步骤6:计算异或校验(仅计算不含校验位的帧体)
+        byte[] tempFrame = bos.toByteArray();
+        byte xor = calculateXorCheck(tempFrame);
+
+        // 步骤7:拼接最终帧(帧体 + 异或校验位)
+        ByteArrayOutputStream finalBos = new ByteArrayOutputStream();
+        finalBos.write(tempFrame);
+        finalBos.write(xor);
+
+        return finalBos.toByteArray();
+    }
+    public static byte[] buildEndReplyFrame(byte[] request) throws IOException {
+        // 步骤1:动态提取地址信息(对齐validateMessage)
+        int[] addrInfo = extractAddrInfo(request);
+        int srcLenFlag = addrInfo[0];
+        int srcAddrByteLen = addrInfo[1]; // 源地址长度 n
+        int srcAddrStart = addrInfo[2];
+        int srcAddrEnd = addrInfo[3];
+        int destLenFlag = addrInfo[4];
+        int destAddrByteLen = addrInfo[5]; // 目的地址长度 m
+        int destAddrStart = addrInfo[6];
+        int destAddrEnd = addrInfo[7];
+
+        // 步骤2:提取地址字节
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // 步骤3:固定字段提取
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 系统识别码(3字节)
+        byte packetSeq = request[5]; // 包序号(1字节)
+
+        // 步骤4:时间戳(4字节)
+        long secondsSince2000 = getSecondsSince2000();
+        byte[] timestamp = new byte[4];
+        timestamp[0] = (byte) ((secondsSince2000 >> 24) & 0xFF);
+        timestamp[1] = (byte) ((secondsSince2000 >> 16) & 0xFF);
+        timestamp[2] = (byte) ((secondsSince2000 >> 8) & 0xFF);
+        timestamp[3] = (byte) (secondsSince2000 & 0xFF);
+
+        // ========== 核心修正:计算包含自身2字节的帧长度字段值 ==========
+        // 帧长度字段值 = 2(自身) + 1(包序号) + 1(帧类型) + 1(源地址标识) + n(源地址) + 1(目的地址标识) + m(目的地址) + 4(时间戳)
+        int frameLenValue =3+ 2 + 1 + 1 + 1 + srcAddrByteLen + 1 + destAddrByteLen + 4+1;
+        // 拆分高低字节
+        byte lenHigh = (byte) ((frameLenValue >> 8) & 0xFF);
+        byte lenLow = (byte) (frameLenValue & 0xFF);
+
+        // 步骤5:构造内容区(包序号到时间戳,不含长度字段)
+        ByteArrayOutputStream content = new ByteArrayOutputStream();
+        content.write(0X80);  // 包序号(1字节)
+        content.write(0x34);       // 帧类型0x34(1字节)
+        content.write(destLenFlag);// 目的地址标识(1字节)
+        content.write(destAddr);   // 目的地址(m字节)
+        content.write(srcLenFlag); // 源地址标识(1字节)srcLenFlag
+        content.write(srcAddr);    // 源地址(n字节)
+        content.write(timestamp);  // 时间戳(4字节)
+        byte[] contentBytes = content.toByteArray();
+
+        // 步骤6:拼帧(系统识别码 + 长度字段 + 内容区)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId);      // 系统识别码(3字节)
+        bos.write(lenHigh);    // 长度高字节(包含自身2字节的总长度)
+        bos.write(lenLow);     // 长度低字节
+        bos.write(contentBytes);
+
+        // 步骤7:计算并添加异或校验(仅计算不含校验位的帧体)
+        byte[] noCheck = bos.toByteArray();
+        byte xor = calculateXorCheck(noCheck);
+        bos.write(xor);
+
+        byte[] result = bos.toByteArray();
+        System.out.println("结束帧应答: " + printHex(result));
+        return result;
+    }
+
+    private static long getSecondsSince2000() {
+        LocalDateTime base = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
+        LocalDateTime now = LocalDateTime.now(ZoneOffset.of("+8"));
+        return Duration.between(base, now).getSeconds();
+    }
+    private static String printHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) sb.append(String.format("%02X ", b));
+        return sb.toString().trim();
+    }
+    /**
+     * 核心工具方法:根据地址标识计算地址字节长度
+     * 规则:标识值转10进制 → 奇数+1 → 除以2
+     */
+    private static int calculateAddrByteLength(int lenFlag) {
+        int temp = lenFlag;
+        // 奇数则+1
+        if (temp % 2 != 0) {
+            temp += 1;
+        }
+        // 除以2得到字节长度
+        return temp / 2;
+    }
+    /**
+     * 核心工具:正向解析源/目的地址信息(贴合实际报文结构)
+     * 逻辑:
+     * 1. 正向定位源地址标识(索引7)、源地址、目的地址标识、目的地址
+     * 2. 找 0x01 2C 验证地址段结束位置,确保解析合法
+     * @return 数组格式:[srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd, destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd]
+     */
+    private static int[] extractAddrInfo(byte[] request) {
+        if (request == null || request.length < 25) { // 适配实际报文最小长度(82字节)
+            throw new IllegalArgumentException("请求帧长度不足,无法解析地址信息,长度=" + (request == null ? 0 : request.length));
+        }
+
+        // ========== 步骤1:正向定位核心索引(贴合协议固定结构) ==========
+        int srcLenIndex = 7; // 源地址标识固定在索引7
+        int srcLenFlag = request[srcLenIndex] & 0xFF;
+        int srcAddrByteLen = calculateAddrByteLength(srcLenFlag);
+        int srcAddrStart = srcLenIndex + 1;
+        int srcAddrEnd = srcAddrStart + srcAddrByteLen;
+
+        // 目的地址标识 = 源地址结束索引
+        int destLenIndex = srcAddrEnd;
+        int destLenFlag = request[destLenIndex] & 0xFF;
+        int destAddrByteLen = calculateAddrByteLength(destLenFlag);
+        int destAddrStart = destLenIndex + 1;
+        int destAddrEnd = destAddrStart + destAddrByteLen;
+
+        // ========== 步骤2:验证地址段结束位置(找 0x01 2C) ==========
+        int deviceCodeIndex = destAddrEnd;
+        // 校验设备编码+功能码是否为 0x01 2C(地址段后必须紧跟)
+        if (deviceCodeIndex + 1 >= request.length
+                || request[deviceCodeIndex] != 0x01
+                || request[deviceCodeIndex + 1] != 0x2C) {
+            throw new IllegalArgumentException("地址段结束后未找到 0x01 2C 序列,地址解析异常:设备编码索引=" + deviceCodeIndex);
+        }
+
+        // ========== 步骤3:范围最终校验 ==========
+        // 源地址范围校验
+        if (srcAddrStart < 8 || srcAddrEnd > destLenIndex) {
+            throw new IllegalArgumentException(
+                    String.format("源地址范围异常:标识索引=%d 标识值=%d 计算长度=%d 起始=%d 结束=%d",
+                            srcLenIndex, srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd));
+        }
+        // 目的地址范围校验
+        if (destAddrStart < (destLenIndex + 1) || destAddrEnd > deviceCodeIndex) {
+            throw new IllegalArgumentException(
+                    String.format("目的地址范围异常:标识索引=%d 标识值=%d 计算长度=%d 起始=%d 结束=%d",
+                            destLenIndex, destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd));
+        }
+
+        return new int[]{
+                srcLenFlag, srcAddrByteLen, srcAddrStart, srcAddrEnd,
+                destLenFlag, destAddrByteLen, destAddrStart, destAddrEnd
+        };
+    }
+
+}