Quellcode durchsuchen

feat(telemetry): 实现平升遥测协议数据解析与校验功能

- 新增 DataCheckUtil 工具类,提供 CRC16、异或校验和累加校验方法
- 新增 DataParser 类,支持平升 6216 协议完整数据帧解析与校验
- 新增 ProtocolUtils 工具类,实现协议时间转换及回应包构建逻辑- 新增 TelemetryApplication 启动类,配置 MyBatis Mapper 扫描路径
- 集成 InvalidMessageException 自定义异常用于协议校验错误处理
林仔 vor 7 Monaten
Ursprung
Commit
af5017d314

+ 15 - 0
telemetry-service/src/main/java/com/zksy/telemetry/TelemetryApplication.java

@@ -0,0 +1,15 @@
+package com.zksy.telemetry;
+
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@MapperScan(basePackages = "com.zksy.telemetry.mapper")
+@SpringBootApplication(scanBasePackages = {"com.zksy.telemetry","com.zksy.api"})
+public class TelemetryApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(TelemetryApplication.class, args);
+        System.out.println("平升遥测终端协议启动成功");
+    }
+}

+ 111 - 0
telemetry-service/src/main/java/com/zksy/telemetry/utils/DataCheckUtil.java

@@ -0,0 +1,111 @@
+package com.zksy.telemetry.utils;
+
+/**
+ * @Description
+ * @Date 2025- 03-06-上午 9:02
+ * @auther tuDouSi
+ */
+public class DataCheckUtil {
+    /**
+     *
+     * @description: crc16校验,输入一个数据,返回一个数组.
+     * @param str
+     * @return 返回两个校验码,低字节在前,高字节在后
+     *
+     */
+    public static String crc16(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda & 0xFF);
+        temdata[1] = (byte) (xda >> 8);
+        return bytesToHexString(temdata);
+    }
+    /**
+     *
+     * @description: crc16校验,
+     * @param str
+     * @return 返回两个校验码,高字节在前,低字节在后
+     *
+     */
+    public static String crc16Tall(String str) {
+        int[] data = hexStringToIntArray(str);
+        int xda, xdapoly;
+        int i, j, xdabit;
+        xda = 0xFFFF;
+        xdapoly = 0xA001;
+        for (i = 0; i < data.length; i++) {
+            xda ^= data[i];
+            for (j = 0; j < 8; j++) {
+                xdabit = (int) (xda & 0x01);
+                xda >>= 1;
+                if (xdabit == 1) {
+                    xda ^= xdapoly;
+                }
+            }
+        }
+
+        // 关键修改:先放高位字节,再放低位字节
+        byte[] temdata = new byte[2];
+        temdata[0] = (byte) (xda >> 8);  // 高位字节
+        temdata[1] = (byte) (xda & 0xFF); // 低位字节
+
+        return bytesToHexString(temdata);
+    }
+    // 将十六进制字符串转换为整数数组
+    private static int[] hexStringToIntArray(String str) {
+        if (str.length() % 2 != 0) {
+            throw new IllegalArgumentException("Hex string length must be even");
+        }
+
+        int[] result = new int[str.length() / 2];
+        for (int i = 0; i < str.length(); i += 2) {
+            result[i / 2] = Integer.parseInt(str.substring(i, i + 2), 16);
+        }
+        return result;
+    }
+
+    // 将字节数组转换为十六进制字符串
+    private static String bytesToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    public static int calcChecksum(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF);
+        }
+        return (0xFF - (sum & 0xFF)) & 0xFF;
+    }
+    /**
+     * 仅对数据累加后取低8位
+     * @param data 待计算字节数组
+     * @param offset 起始索引
+     * @param length 计算长度
+     * @return 8位校验结果
+     */
+    static int calcSumOnly(byte[] data, int offset, int length) {
+        int sum = 0;
+        for (int i = offset; i < offset + length; i++) {
+            sum += (data[i] & 0xFF); // 累加每个字节的无符号值
+        }
+        return sum & 0xFF; // 仅取低8位,无FF-操作
+    }
+}

+ 205 - 0
telemetry-service/src/main/java/com/zksy/telemetry/utils/DataParser.java

@@ -0,0 +1,205 @@
+package com.zksy.telemetry.utils;
+
+import com.zksy.common.exception.InvalidMessageException;
+import com.zksy.telemetry.domain.TelemetryData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+
+public class DataParser {
+
+    private static final Logger logger = LoggerFactory.getLogger(DataParser.class);
+
+    /**
+     * 平升6216协议完整校验:
+     * 1. 长度检查
+     * 2. 系统识别码检查
+     * 3. 异或校验(外层 PS 协议)
+     * 4. CRC16 校验(内层 历史记录协议)
+     */
+    public static void validateMessage(byte[] msgBytes) throws InvalidMessageException {
+        // 1. 长度检查
+        if (msgBytes == null || msgBytes.length < 10) {
+            throw new InvalidMessageException("数据帧长度过短");
+        }
+
+        // 2. 帧头与系统识别码检查
+        if (msgBytes[0] != 0x12 || msgBytes[1] != 0x34 || msgBytes[2] != 0x56) {
+            throw new InvalidMessageException("系统识别码错误,应为 12 34 56");
+        }
+
+        // 3. 校验整帧长度(从系统识别码到最后1字节)
+        int declaredLen = ((msgBytes[3] & 0xFF) << 8) | (msgBytes[4] & 0xFF);
+        if (declaredLen != msgBytes.length) {
+            throw new InvalidMessageException(String.format(
+                    "帧长度不匹配:声明=%d 实际=%d", declaredLen, msgBytes.length));
+        }
+
+        // 4. 异或校验(最后一字节)
+        byte receivedXor = msgBytes[msgBytes.length - 1];
+        byte[] xorData = Arrays.copyOfRange(msgBytes, 0, msgBytes.length - 1);
+        byte calculatedXor = ProtocolUtils.calculateXorCheck(xorData);
+        if (receivedXor != calculatedXor) {
+            throw new InvalidMessageException(String.format(
+                    "异或校验失败:计算=0x%02X 接收=0x%02X", calculatedXor, receivedXor));
+        }
+
+        // 5. 仅对上报历史记录帧(0x31)进行 CRC 校验
+        byte frameType = msgBytes[6];
+        if (frameType == 0x31) {
+            // CRC 高低字节在帧末尾的倒数第3、第2字节(倒数第1是异或)
+            int crcEndIndex = msgBytes.length - 3;
+            int crcHighIndex = crcEndIndex;
+            int crcLowIndex = crcEndIndex + 1;
+
+            if (crcHighIndex <= 0) {
+                throw new InvalidMessageException("CRC数据段索引错误");
+            }
+
+            // 设备编码开始(内层协议起点):
+            // 外层结构 3+2+1+1+1+n+1+m => 源地址/目的地址结束后紧接设备编码(0x01)
+            // 简化方式:找到第一个 0x01 2C 序列即可(设备编码 + 功能码)
+            int deviceCodeIndex = -1;
+            for (int i = 0; i < msgBytes.length - 1; i++) {
+                if (msgBytes[i] == 0x01 && msgBytes[i + 1] == 0x2C) {
+                    deviceCodeIndex = i;
+                    break;
+                }
+            }
+
+            if (deviceCodeIndex == -1) {
+                throw new InvalidMessageException("未找到设备编码起点 (0x01 2C)");
+            }
+
+            byte[] crcData = Arrays.copyOfRange(msgBytes, deviceCodeIndex, crcEndIndex);
+            int calculatedCrc = ProtocolUtils.calculateCRC16(crcData);
+            int receivedCrc = ((msgBytes[crcLowIndex] & 0xFF) << 8) | (msgBytes[crcHighIndex] & 0xFF);
+
+            if (calculatedCrc != receivedCrc) {
+                throw new InvalidMessageException(String.format(
+                        "CRC校验失败:计算=0x%04X 接收=0x%04X", calculatedCrc, receivedCrc));
+            }
+        }
+
+        logger.debug("消息通过所有校验");
+    }
+    /**
+     * 解析消息(支持历史记录帧0x31和结束通讯帧0x34,协议2.2.3)
+     */
+    public static TelemetryData parseMessage(byte[] msgBytes) {
+        TelemetryData data = new TelemetryData();
+        int index = 0;
+
+        // 1. 系统识别码
+        data.setSystemIdentifier(String.format("%02X%02X%02X", msgBytes[0], msgBytes[1], msgBytes[2]));
+        index += 3;
+
+        // 2. 帧长度
+        int frameLength = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+        data.setFrameLength(String.valueOf(frameLength));
+        index += 2;
+
+        // 3. 包序号、帧类型
+        data.setPacketSequence(String.format("%02X", msgBytes[index++]));
+        byte frameType = msgBytes[index++];
+        data.setDataType(String.format("0x%02X", frameType));
+
+        // 4. 源地址长度 + 内容
+        int srcLen = msgBytes[index++] & 0xFF;
+        // 实际有些设备固定6字节地址,防止越界
+        int srcAddrSize = Math.min(srcLen, 6);
+        byte[] srcAddr = Arrays.copyOfRange(msgBytes, index, index + srcAddrSize);
+        data.setSourceAddr(parseBcdToStr(srcAddr));
+        index += srcAddrSize;
+
+        // 5. 目的地址长度 + 内容
+        int dstLen = msgBytes[index++] & 0xFF;
+        int dstAddrSize = Math.min(dstLen, 6);
+        byte[] dstAddr = Arrays.copyOfRange(msgBytes, index, index + dstAddrSize);
+        data.setDestAddr(parseBcdToStr(dstAddr));
+        data.setDestAddrLength(String.valueOf(dstAddrSize));
+        index += dstAddrSize;
+
+        // 6. 帧类型 = 0x31
+        if (frameType == 0x31) {
+            data.setDeviceCode(String.format("%02X", msgBytes[index++]));
+            data.setFunctionCode(String.format("%02X", msgBytes[index++]));
+
+            data.setReserve1(String.format("%02X%02X%02X%02X", msgBytes[index], msgBytes[index + 1], msgBytes[index + 2], msgBytes[index + 3]));
+            index += 4;
+            data.setReserve2(String.format("%02X%02X", msgBytes[index], msgBytes[index + 1]));
+            index += 2;
+
+            int recordCount = msgBytes[index++] & 0xFF;
+            data.setRecordCount(String.valueOf(recordCount));
+
+            int recordFormat = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+            data.setRecordFormat(String.format("%04X", recordFormat));
+            index += 2;
+
+            int powerVolt = ((msgBytes[index] & 0xFF) << 8) | (msgBytes[index + 1] & 0xFF);
+            data.setPowerVoltage(String.format("%.2f", powerVolt / 100.0));
+            index += 2;
+
+            byte fieldStatus = msgBytes[index++];
+            data.setFieldStatus(String.format("%02X", fieldStatus));
+            data.setIsLastPacket((fieldStatus & 0x08) != 0);
+
+            data.setProtocolVersion(String.format("%02X", msgBytes[index++]));
+            data.setParamVersion(String.format("%02X", msgBytes[index++]));
+            data.setSignalQuality(String.valueOf(msgBytes[index++] & 0xFF));
+
+            data.setReserve3(String.format("%02X%02X%02X", msgBytes[index], msgBytes[index + 1], msgBytes[index + 2]));
+            index += 3;
+
+            // --- 历史记录部分 ---
+            if (recordCount > 0 && index + 4 <= msgBytes.length) {
+                int ts = ((msgBytes[index] & 0xFF) << 24) | ((msgBytes[index + 1] & 0xFF) << 16)
+                        | ((msgBytes[index + 2] & 0xFF) << 8) | (msgBytes[index + 3] & 0xFF);
+                LocalDateTime recordTime = ProtocolUtils.convertToDateTime(ts);
+                data.setTimestampSince(recordTime);
+                index += 4;
+
+                long net = bytesToUInt32(msgBytes, index); index += 4;
+                long pos = bytesToUInt32(msgBytes, index); index += 4;
+                long neg = bytesToUInt32(msgBytes, index); index += 4;
+                long flow = bytesToUInt32(msgBytes, index); index += 4;
+                long pressure = bytesToUInt32(msgBytes, index); index += 4;
+                long switchVal = bytesToUInt32(msgBytes, index); index += 4;
+
+                data.setMeter1NetTotal(String.valueOf(net));
+                data.setMeter1PositiveTotal(String.valueOf(pos));
+                data.setMeter1NegativeTotal(String.valueOf(neg));
+                data.setMeter1InstantFlow(String.valueOf(flow));
+                data.setPressure(String.valueOf(pressure));
+                data.setSwitchValue(String.format("%08X", switchVal));
+
+                data.setHistoryRecords(String.format(
+                        "时间:%s,净累计:%d,正累计:%d,负累计:%d,瞬时流量:%d,压力:%d,开关量:%08X",
+                        recordTime, net, pos, neg, flow, pressure, switchVal));
+            }
+        }
+
+        data.setCreateTime(LocalDateTime.now());
+        return data;
+    }
+
+    /** 工具方法:BCD转字符串 */
+    private static String parseBcdToStr(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02X", b));
+        }
+        return sb.toString();
+    }
+
+    /** 工具方法:4字节无符号整型 */
+    private static long bytesToUInt32(byte[] data, int offset) {
+        return ((long) (data[offset] & 0xFF) << 24)
+                | ((long) (data[offset + 1] & 0xFF) << 16)
+                | ((long) (data[offset + 2] & 0xFF) << 8)
+                | ((long) (data[offset + 3] & 0xFF));
+    }
+}

+ 36 - 0
telemetry-service/src/main/java/com/zksy/telemetry/utils/DeviceOfflineCheckTask.java

@@ -0,0 +1,36 @@
+package com.zksy.telemetry.utils;/*
+package com.zksy.gasTransmitter.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class DeviceOfflineCheckTask {
+    private static final Logger logger = LoggerFactory.getLogger(DeviceOfflineCheckTask.class);
+
+    // 用于存储设备编号及其最后一次接收数据的时间
+    public static ConcurrentHashMap<String, Date> deviceLastReceiveTimeMap = new ConcurrentHashMap<>();
+    @Autowired
+    private BaseDevicesService baseDevicesService;
+
+    // 定时检查设备是否离线
+    @Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每24小时执行一次
+    public void checkDeviceOffline() {
+        Date now = new Date();
+        for (Map.Entry<String, Date> entry : deviceLastReceiveTimeMap.entrySet()) {
+            long diff = now.getTime() - entry.getValue().getTime();
+            // 如果设备在 23 小时内没有接收数据,则认为设备离线
+            if (diff > 23 * 60 * 60 * 1000) {
+                baseDevicesService.getByDeviceNumberStatus(entry.getKey(), 1, 0);
+                logger.info("设备 {} 已离线", entry.getKey());
+            }
+        }
+    }
+}*/

+ 305 - 0
telemetry-service/src/main/java/com/zksy/telemetry/utils/ProtocolUtils.java

@@ -0,0 +1,305 @@
+package com.zksy.telemetry.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;
+
+public class ProtocolUtils {
+    /**
+     * 将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) {
+        // 提取源、目的地址
+        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;
+
+        // 结束通讯帧包序号固定 0x80
+        byte packetSeq = (byte) 0x80;
+
+        byte[] response = new byte[3 + 2 + 1 + 1 + 1 + dstByteLen + 1 + srcByteLen + 1];
+        int idx = 0;
+
+        // 系统识别码
+        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++] = packetSeq;
+
+        // 帧类型 0x34
+        response[idx++] = 0x34;
+
+        // 源地址 = 原帧目的地址
+        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;
+    }
+
+    public static byte[] buildErrorResponse(String msg) {
+        byte[] text = msg.getBytes();
+        byte[] data = new byte[3 + 2 + 1 + text.length + 1];
+        int idx = 0;
+        data[idx++] = 0x12;
+        data[idx++] = 0x34;
+        data[idx++] = 0x56;
+
+        int len = data.length;
+        data[idx++] = (byte) ((len >> 8) & 0xFF);
+        data[idx++] = (byte) (len & 0xFF);
+
+        data[idx++] = 0x7F; // 错误类型标识
+        System.arraycopy(text, 0, data, idx, text.length);
+        idx += text.length;
+
+        data[idx++] = calculateXorCheck(Arrays.copyOfRange(data, 0, idx - 1));
+        return data;
+    }
+    public static byte[] buildCustomReplyFrame(byte[] request) throws IOException {
+        // 校验长度至少20个字节
+        if (request.length < 20) {
+            throw new IllegalArgumentException("请求帧长度不足,无法构造应答");
+        }
+
+        // 提取关键字段
+        byte[] header = Arrays.copyOfRange(request, 0, 4); // 系统识别码
+        byte packetSeq = request[5];                       // 包序号
+        byte frameType = request[6];                       // 帧类型(0x31)
+        byte addrLen = request[7];                         // 源地址长度(0B)
+        int srcAddrStart = 8;
+        int srcAddrEnd = srcAddrStart + 6;                 // 实际取6字节源地址
+        int destAddrStart = srcAddrEnd + 1;                // 跳过目的地址长度0B
+        int destAddrEnd = destAddrStart + 6;
+
+        byte[] srcAddr = Arrays.copyOfRange(request, srcAddrStart, srcAddrEnd);
+        byte[] destAddr = Arrays.copyOfRange(request, destAddrStart, destAddrEnd);
+
+        // --- 构造应答帧体 ---
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+        // 前四字节
+        bos.write(header, 0, 4);
+        // 帧长度 0x16
+        bos.write(new byte[]{0x16});
+        // 包序号、帧类型
+        bos.write(packetSeq);
+        bos.write(frameType);
+        bos.write(addrLen);
+
+        // 交换地址段:目的 → 源
+        bos.write(destAddr);   // 目的地址放前
+        bos.write(addrLen);
+        bos.write(srcAddr);    // 源地址放后
+
+        // 计算异或校验码
+        byte[] withoutCheck = bos.toByteArray();
+        byte xor = 0x00;
+        for (byte b : withoutCheck) xor ^= b;
+        bos.write(xor);
+
+        byte[] result = bos.toByteArray();
+        return result;
+    }
+    public static byte[] buildEndReplyFrame(byte[] request) throws IOException {
+        if (request == null || request.length < 25) {
+            throw new IllegalArgumentException("请求帧长度不足,无法构造结束应答");
+        }
+
+        // 1 系统识别码
+        byte[] sysId = Arrays.copyOfRange(request, 0, 3); // 12 34 56
+
+        // 2 包序号
+        byte packetSeq = request[5]; // 81
+
+        // 3 源/目的地址
+        byte srcLen = request[7];
+        byte[] srcAddr = Arrays.copyOfRange(request, 8, 14);
+        byte destLen = request[14];
+        byte[] destAddr = Arrays.copyOfRange(request, 15, 21);
+
+        // 4 当前时间转换为从 2000-01-01 开始的秒数
+        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);
+
+        // 5 构造内容区(从包序号开始,到时间戳结束)
+        ByteArrayOutputStream content = new ByteArrayOutputStream();
+        content.write(packetSeq);  // 包序号
+        content.write(0x34);       // 帧类型(替换成 0x34)
+        content.write(srcLen);     // 源地址长度
+        content.write(srcAddr);    // 源地址
+        content.write(destLen);    // 目的地址长度
+        content.write(destAddr);   // 目的地址
+        content.write(timestamp);  // 当前时间戳
+        byte[] contentBytes = content.toByteArray();
+
+        // 6 计算帧长度(从包序号到时间戳的长度)
+        int len = contentBytes.length;
+        byte lenHigh = (byte) ((len >> 8) & 0xFF);
+        byte lenLow = (byte) (len & 0xFF);
+
+        // 7 拼完整帧(不含校验)
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bos.write(sysId);      // 系统识别码
+        bos.write(lenHigh);    // 帧长度高
+        bos.write(lenLow);     // 帧长度低
+        bos.write(contentBytes);
+
+        // 8 计算异或校验
+        byte[] noCheck = bos.toByteArray();
+        byte xor = 0x00;
+        for (byte b : noCheck) xor ^= b;
+        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();
+    }
+}