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; 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 }; } }