411 lines
16 KiB
Java
411 lines
16 KiB
Java
package com.evobms.project.bms.controller;
|
||
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
|
||
import javax.crypto.Cipher;
|
||
import javax.crypto.spec.SecretKeySpec;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.Arrays;
|
||
|
||
public class GBT32960FullDecoder {
|
||
private static final Logger log = LoggerFactory.getLogger(GBT32960FullDecoder.class);
|
||
|
||
// 从SN码生成AES密钥
|
||
private static byte[] generateKeyFromSn(String sn) {
|
||
String s = sn == null ? "" : sn;
|
||
String key;
|
||
if ("EVO0001".equals(s)) {
|
||
StringBuilder sb = new StringBuilder(16);
|
||
sb.append("EVO0001");
|
||
while (sb.length() < 16) sb.append('0');
|
||
key = sb.toString();
|
||
} else if (s.length() >= 16) {
|
||
key = s.substring(s.length() - 16);
|
||
} else {
|
||
StringBuilder sb = new StringBuilder(16);
|
||
sb.append(s);
|
||
while (sb.length() < 16) sb.append('0');
|
||
key = sb.toString();
|
||
}
|
||
return key.getBytes(StandardCharsets.UTF_8);
|
||
}
|
||
|
||
// AES解密
|
||
private static byte[] decryptData(byte[] encryptedData, byte[] key) throws Exception {
|
||
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
|
||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||
return cipher.doFinal(encryptedData);
|
||
}
|
||
|
||
// BCC校验
|
||
private static byte calculateBCC(byte[] data, int start, int end) {
|
||
byte bcc = 0;
|
||
for (int i = start; i < end; i++) {
|
||
bcc ^= data[i];
|
||
}
|
||
return bcc;
|
||
}
|
||
|
||
// 16进制字符串转字节数组
|
||
private static byte[] hexStringToByteArray(String hex) {
|
||
hex = hex.replaceAll("\\s", ""); // 移除空格
|
||
int len = hex.length();
|
||
byte[] data = new byte[len / 2];
|
||
for (int i = 0; i < len; i += 2) {
|
||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
||
+ Character.digit(hex.charAt(i + 1), 16));
|
||
}
|
||
return data;
|
||
}
|
||
|
||
// 字节数组转16进制字符串
|
||
private static String bytesToHex(byte[] bytes) {
|
||
StringBuilder sb = new StringBuilder();
|
||
for (byte b : bytes) {
|
||
sb.append(String.format("%02X", b));
|
||
}
|
||
return sb.toString();
|
||
}
|
||
|
||
// 详细的协议解析方法
|
||
public static String parseGBT32960Data(byte[] receivedData, String deviceSn) {
|
||
String bytesData = null;
|
||
try {
|
||
log.info("=== GBT32960协议数据解析 ===");
|
||
log.info("实际数据长度: " + receivedData.length + " 字节");
|
||
// 1. 检查起始位
|
||
if (receivedData.length < 2 || receivedData[0] != 0x23 || receivedData[1] != 0x23) {
|
||
log.error("无效的起始位或数据过短");
|
||
return null;
|
||
}
|
||
log.info("起始位: 0x2323 ✓");
|
||
|
||
// 2. 打印完整数据用于调试
|
||
log.info("完整数据: " + bytesToHex(receivedData));
|
||
|
||
// 3. 分析协议结构
|
||
int offset = 2; // 跳过起始位
|
||
|
||
// 命令标识
|
||
byte commandId = receivedData[offset++];
|
||
log.info("命令标识: 0x" + String.format("%02X", commandId));
|
||
|
||
// 应答标志
|
||
byte responseFlag = receivedData[offset++];
|
||
log.info("应答标志: 0x" + String.format("%02X", responseFlag));
|
||
|
||
// VIN码 (17字节)
|
||
byte[] vin = Arrays.copyOfRange(receivedData, offset, offset + 17);
|
||
offset += 17;
|
||
log.info("VIN码: " + bytesToHex(vin) + " (" + new String(vin).trim() + ")");
|
||
|
||
// 加密方式
|
||
byte encryptionMethod = receivedData[offset++];
|
||
log.info("加密方式: 0x" + String.format("%02X", encryptionMethod));
|
||
|
||
if (encryptionMethod != 0x03) {
|
||
log.error("不支持的解密方式,期望AES128(0x03)");
|
||
return null;
|
||
}
|
||
|
||
// 数据单元长度 (WORD, 大端序)
|
||
int dataUnitLength = ((receivedData[offset] & 0xFF) << 8) | (receivedData[offset + 1] & 0xFF);
|
||
offset += 2;
|
||
log.info("声明的数据单元长度: " + dataUnitLength + " 字节");
|
||
|
||
// 计算当前头部长度
|
||
int headerLength = offset;
|
||
log.info("协议头部长度: " + headerLength + " 字节");
|
||
|
||
// 剩余数据长度
|
||
int remainingDataLength = receivedData.length - headerLength;
|
||
log.info("剩余数据长度: " + remainingDataLength + " 字节");
|
||
|
||
// 4. 重新计算期望长度
|
||
// 方案1: 如果BCC是1字节
|
||
int expectedLength1 = headerLength + dataUnitLength + 1;
|
||
// 方案2: 如果BCC是2字节(根据您最初描述)
|
||
int expectedLength2 = headerLength + dataUnitLength + 2;
|
||
|
||
log.info("期望长度 (BCC=1字节): " + expectedLength1);
|
||
log.info("期望长度 (BCC=2字节): " + expectedLength2);
|
||
log.info("实际长度: " + receivedData.length);
|
||
|
||
// 5. 确定正确的数据范围
|
||
int encryptedDataStart = offset;
|
||
int encryptedDataEnd;
|
||
int bccStart;
|
||
|
||
if (receivedData.length == expectedLength1) {
|
||
// BCC为1字节
|
||
encryptedDataEnd = encryptedDataStart + dataUnitLength;
|
||
bccStart = encryptedDataEnd;
|
||
log.info("采用方案: BCC为1字节");
|
||
} else if (receivedData.length == expectedLength2) {
|
||
// BCC为2字节
|
||
encryptedDataEnd = encryptedDataStart + dataUnitLength;
|
||
bccStart = encryptedDataEnd;
|
||
log.info("采用方案: BCC为2字节");
|
||
} else {
|
||
// 自动适应:使用剩余的所有数据作为加密数据
|
||
encryptedDataEnd = receivedData.length - 1; // 假设BCC为1字节
|
||
int actualDataUnitLength = encryptedDataEnd - encryptedDataStart;
|
||
log.info("长度不匹配,使用实际数据单元长度: " + actualDataUnitLength + " 字节");
|
||
dataUnitLength = actualDataUnitLength;
|
||
bccStart = encryptedDataEnd;
|
||
}
|
||
|
||
// 6. 提取加密数据
|
||
byte[] encryptedData = Arrays.copyOfRange(receivedData, encryptedDataStart, encryptedDataStart + dataUnitLength);
|
||
log.info("加密数据长度: " + encryptedData.length + " 字节");
|
||
log.info("加密数据 (前64字节): " + bytesToHex(Arrays.copyOf(encryptedData, Math.min(64, encryptedData.length))) + "...");
|
||
if (encryptedData.length % 16 != 0) {
|
||
log.error("AES密文长度非法,不是16倍数,length=" + encryptedData.length);
|
||
return null;
|
||
}
|
||
// 7. 提取BCC校验码
|
||
int bccLength = receivedData.length - (encryptedDataStart + dataUnitLength);
|
||
byte[] receivedBCC = Arrays.copyOfRange(receivedData, encryptedDataStart + dataUnitLength, receivedData.length);
|
||
//log.info("BCC长度: " + bccLength + " 字节");
|
||
//log.info("接收到的BCC: " + bytesToHex(receivedBCC));
|
||
|
||
// 8. 计算BCC校验范围:从命令单元开始到加密数据结束
|
||
int bccCalcStart = 2; // 从命令标识开始 (跳过0x2323)
|
||
int bccCalcEnd = encryptedDataStart + dataUnitLength;
|
||
byte calculatedBCC = calculateBCC(receivedData, bccCalcStart, bccCalcEnd);
|
||
|
||
log.info("BCC计算范围: 字节[" + bccCalcStart + "~" + bccCalcEnd + ")");
|
||
log.info("计算出的BCC: 0x" + String.format("%02X", calculatedBCC));
|
||
|
||
// 验证BCC (只比较第一个字节)
|
||
if (receivedBCC.length > 0 && calculatedBCC == receivedBCC[0]) {
|
||
log.info("BCC校验通过 ✓");
|
||
} else {
|
||
log.error("BCC校验失败!");
|
||
}
|
||
|
||
|
||
//使用的AES密钥
|
||
byte[] aesKey = generateKeyFromSn(deviceSn);
|
||
System.out.println(Arrays.toString(aesKey));
|
||
System.out.println(aesKey.length);
|
||
log.info("AES密钥: " + bytesToHex(aesKey));
|
||
byte[] decryptedData = decryptData(encryptedData, aesKey);
|
||
bytesData = bytesToHex(decryptedData);
|
||
log.info("解密成功!");
|
||
log.info("解密后数据长度: " + decryptedData.length + " 字节");
|
||
log.info("解密后数据 (完整): " + bytesData);
|
||
|
||
|
||
// 10. 解析解密后的数据单元
|
||
// parseDecryptedDataUnit(decryptedData);
|
||
|
||
} catch (Exception e) {
|
||
log.error("解析过程中发生错误: " + e.getMessage());
|
||
e.printStackTrace();
|
||
}
|
||
return bytesData;
|
||
}
|
||
|
||
/* public static String parseGBT32960Data(byte[] frame, String deviceSn) throws Exception {
|
||
|
||
if (frame == null || frame.length < 25) {
|
||
throw new IllegalArgumentException("报文长度非法");
|
||
}
|
||
int offset = 0;
|
||
// 1. 起始符校验 0x23 0x23
|
||
|
||
if (frame[offset++] != 0x23 || frame[offset++] != 0x23) {
|
||
throw new IllegalArgumentException("非法起始位,非0x2323");
|
||
}
|
||
// 2. 命令标识
|
||
byte commandId = frame[offset++];
|
||
// 3. 应答标志
|
||
byte responseFlag = frame[offset++];
|
||
// 4. VIN (17字节)
|
||
byte[] vinBytes = Arrays.copyOfRange(frame, offset, offset + 17);
|
||
String vin = new String(vinBytes).trim();
|
||
offset += 17;
|
||
// 5. 加密方式
|
||
byte encryptType = frame[offset++];
|
||
// 6. 数据单元长度 (大端)
|
||
if (offset + 2 > frame.length) {
|
||
throw new IllegalArgumentException("数据单元长度字段越界");
|
||
}
|
||
int dataUnitLength = ((frame[offset] & 0xFF) << 8) | (frame[offset + 1] & 0xFF);
|
||
offset += 2;
|
||
int headerLength = offset;
|
||
|
||
// 7. 校验整体长度
|
||
// 标准格式:
|
||
// 头部 + 数据单元 + 1字节BCC
|
||
int expectedLength = headerLength + dataUnitLength + 1;
|
||
if (frame.length != expectedLength) {
|
||
throw new IllegalArgumentException("报文长度不匹配,声明长度=" + dataUnitLength + ",实际长度=" + frame.length + ",期望总长度=" + expectedLength);
|
||
}
|
||
// 8. 提取数据单元
|
||
byte[] dataUnit = Arrays.copyOfRange(frame, headerLength, headerLength + dataUnitLength
|
||
);
|
||
// 9. BCC 校验
|
||
// BCC范围:从命令标识开始
|
||
byte receivedBcc = frame[frame.length - 1];
|
||
byte calculatedBcc = calculateBCC(frame, 2, headerLength + dataUnitLength
|
||
);
|
||
if (receivedBcc != calculatedBcc) {
|
||
throw new IllegalStateException(
|
||
String.format("BCC校验失败,收到=0x%02X 计算=0x%02X",
|
||
receivedBcc, calculatedBcc)
|
||
);
|
||
}
|
||
|
||
|
||
// 10. 加密处理
|
||
|
||
if (encryptType == 0x01) {
|
||
// 不加密
|
||
return bytesToHex(dataUnit);
|
||
}
|
||
|
||
if (encryptType != 0x03) {
|
||
throw new UnsupportedOperationException(
|
||
"不支持的加密方式: " + encryptType);
|
||
}
|
||
|
||
// AES 必须 16倍数
|
||
if (dataUnitLength % 16 != 0) {
|
||
throw new IllegalStateException(
|
||
"AES密文长度非法,不是16倍数,length=" + dataUnitLength);
|
||
}
|
||
|
||
|
||
// 11. AES解密
|
||
|
||
byte[] aesKey = generateKeyFromSn(deviceSn);
|
||
|
||
byte[] decrypted = decryptData(dataUnit, aesKey);
|
||
|
||
return bytesToHex(decrypted);
|
||
}
|
||
*/
|
||
|
||
private static void parseDecryptedDataUnit(byte[] decryptedData) {
|
||
log.info("\n=== 数据单元解析 ===");
|
||
|
||
if (decryptedData == null || decryptedData.length == 0) {
|
||
log.info("解密数据为空");
|
||
return;
|
||
}
|
||
|
||
log.info("数据单元总长度: " + decryptedData.length + " 字节");
|
||
log.info("数据单元内容: " + bytesToHex(decryptedData));
|
||
|
||
// 尝试解析采集时间(前6字节BCD)
|
||
if (decryptedData.length >= 6) {
|
||
String ts = parseTimestampFromBCD(Arrays.copyOfRange(decryptedData, 0, 6));
|
||
log.info("采集时间: " + ts + " (BCD)");
|
||
}
|
||
|
||
// 扫描数据块: [类型ID(1)] [长度(1)]
|
||
int offset = 6; // 时间之后
|
||
int itemIndex = 0;
|
||
while (offset + 2 <= decryptedData.length) {
|
||
int typeId = decryptedData[offset] & 0xFF;
|
||
int length = decryptedData[offset + 1] & 0xFF;
|
||
int payloadStart = offset + 2;
|
||
int payloadEnd = payloadStart + length;
|
||
|
||
// 长度异常时,尝试步进1字节继续
|
||
if (payloadEnd > decryptedData.length) {
|
||
// 可能当前字节并非数据块起点(例如信息标志位),小步扫描跳过
|
||
offset += 1;
|
||
continue;
|
||
}
|
||
|
||
byte[] payload = Arrays.copyOfRange(decryptedData, payloadStart, payloadEnd);
|
||
log.info(String.format("数据块[%d] 类型: %s, 长度: %d, 起止: %d-%d", itemIndex++, typeName(typeId), length, payloadStart, payloadEnd - 1));
|
||
log.info("数据块HEX: " + bytesToHex(Arrays.copyOf(payload, Math.min(64, payload.length))) + (payload.length > 64 ? "..." : ""));
|
||
|
||
// 极值数据尝试详细解析
|
||
if (typeId == 0x06) {
|
||
try {
|
||
GBT32960ExtremeDataParser.ExtremeData extreme = GBT32960ExtremeDataParser.parseExtremeData(Arrays.copyOfRange(decryptedData, offset, payloadEnd), 0);
|
||
if (extreme != null) {
|
||
log.info("极值数据解析结果: " + extreme.toString());
|
||
}
|
||
} catch (Exception ex) {
|
||
log.warn("极值数据解析异常: " + ex.getMessage());
|
||
}
|
||
}
|
||
|
||
offset = payloadEnd;
|
||
}
|
||
|
||
// 兼容旧的极值扫描逻辑
|
||
GBT32960ExtremeDataExtractor.ExtremeData andParseExtremeData = GBT32960ExtremeDataExtractor.findAndParseExtremeData(decryptedData);
|
||
if (andParseExtremeData != null) {
|
||
log.info(andParseExtremeData.toString());
|
||
} else {
|
||
log.info("未找到极值数据或解析失败");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 跳过数据项
|
||
*/
|
||
private static int skipDataItem(byte[] data, int offset) {
|
||
if (offset >= data.length) return offset;
|
||
|
||
offset++; // 跳过数据类型
|
||
if (offset < data.length) {
|
||
byte length = data[offset];
|
||
offset += 1 + (length & 0xFF); // 跳过长度字节和数据内容
|
||
}
|
||
|
||
return offset;
|
||
}
|
||
|
||
private static String typeName(int typeId) {
|
||
switch (typeId & 0xFF) {
|
||
case 0x01:
|
||
return "整车数据";
|
||
case 0x02:
|
||
return "驱动电机数据";
|
||
case 0x03:
|
||
return "燃料电池数据";
|
||
case 0x04:
|
||
return "发动机数据";
|
||
case 0x05:
|
||
return "车辆位置数据";
|
||
case 0x06:
|
||
return "极值数据";
|
||
case 0x07:
|
||
return "报警数据";
|
||
case 0x08:
|
||
return "储能装置电压数据";
|
||
case 0x09:
|
||
return "储能装置温度数据";
|
||
default:
|
||
return String.format("未知类型(0x%02X)", typeId);
|
||
}
|
||
}
|
||
|
||
// BCD字节转十进制整数
|
||
private static int bcdToInt(byte b) {
|
||
return ((b >> 4) & 0x0F) * 10 + (b & 0x0F);
|
||
}
|
||
|
||
// 解析采集时间(BCD YY MM DD hh mm ss)
|
||
private static String parseTimestampFromBCD(byte[] data) {
|
||
if (data == null || data.length < 6) return "未知";
|
||
int year = 2000 + bcdToInt(data[0]);
|
||
int month = bcdToInt(data[1]);
|
||
int day = bcdToInt(data[2]);
|
||
int hour = bcdToInt(data[3]);
|
||
int minute = bcdToInt(data[4]);
|
||
int second = bcdToInt(data[5]);
|
||
return String.format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
|
||
}
|
||
}
|