969 lines
39 KiB
Java
969 lines
39 KiB
Java
package com.evobms.project.bms.service;
|
||
|
||
import com.alibaba.fastjson2.JSON;
|
||
import com.evobms.project.Battery.domain.ExtremeValues;
|
||
import com.evobms.project.Battery.domain.SubsystemVoltage;
|
||
import com.evobms.project.Battery.domain.SubsystemTemperature;
|
||
import com.evobms.project.Battery.service.IExtremeValuesService;
|
||
import com.evobms.project.Battery.service.ISubsystemVoltageService;
|
||
import com.evobms.project.Battery.service.ISubsystemTemperatureService;
|
||
import com.evobms.project.Battery.domain.ChargeDischargeSummary;
|
||
import com.evobms.project.Battery.service.IChargeDischargeSummaryService;
|
||
import com.evobms.project.VehicleData.domain.VehicleData;
|
||
import com.evobms.project.VehicleData.domain.VehicleLocation;
|
||
import com.evobms.project.VehicleData.service.IVehicleDataService;
|
||
import com.evobms.project.VehicleData.service.IVehicleLocationService;
|
||
import com.evobms.project.bms.domain.BmsDevice;
|
||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.beans.factory.annotation.Value;
|
||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||
import org.springframework.integration.support.MessageBuilder;
|
||
import org.springframework.messaging.MessageChannel;
|
||
import org.springframework.stereotype.Service;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
|
||
import java.math.BigDecimal;
|
||
import java.util.*;
|
||
import javax.crypto.Cipher;
|
||
import javax.crypto.spec.SecretKeySpec;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.security.GeneralSecurityException;
|
||
import java.util.stream.Collectors;
|
||
|
||
import com.evobms.project.system.service.IBmsDevicesService;
|
||
import com.evobms.project.system.domain.BmsDevices;
|
||
|
||
import static com.evobms.project.bms.controller.GBT32960FullDecoder.parseGBT32960Data;
|
||
import static oshi.util.ParseUtil.hexStringToByteArray;
|
||
|
||
/**
|
||
* MQTT服务类
|
||
* 只有在mqtt.enabled=true时才会加载此服务
|
||
*
|
||
* @author evobms
|
||
*/
|
||
@Service
|
||
@ConditionalOnProperty(name = "mqtt.enabled", havingValue = "true")
|
||
public class MqttService {
|
||
private static final Logger log = LoggerFactory.getLogger(MqttService.class);
|
||
|
||
@Autowired
|
||
private MessageChannel mqttOutboundChannel;
|
||
|
||
@Autowired
|
||
private IBmsDevicesService bmsDevicesService;
|
||
|
||
@Autowired
|
||
private IVehicleDataService vehicleDataService;
|
||
|
||
@Autowired
|
||
private IVehicleLocationService vehicleLocationService;
|
||
|
||
@Autowired
|
||
private IExtremeValuesService extremeValuesService;
|
||
|
||
@Autowired
|
||
private ISubsystemVoltageService subsystemVoltageService;
|
||
|
||
@Autowired
|
||
private ISubsystemTemperatureService subsystemTemperatureService;
|
||
|
||
@Autowired
|
||
private IChargeDischargeSummaryService chargeDischargeSummaryService;
|
||
|
||
|
||
@Value("${mqtt.publishTopic}")
|
||
private String publishTopic;
|
||
|
||
@Value("${mqtt.defaultQos}")
|
||
private int defaultQos;
|
||
|
||
@Value("${mqtt.dataHeaderLength:12}")
|
||
private int dataHeaderLength;
|
||
|
||
/**
|
||
* 发送MQTT消息
|
||
*
|
||
* @param topic 主题
|
||
* @param payload 消息内容
|
||
*/
|
||
public void sendMessage(String topic, String payload) {
|
||
log.info("发送MQTT消息 -> 主题: {}, QOS: {}, 载荷: {}", topic, defaultQos, payload);
|
||
mqttOutboundChannel.send(MessageBuilder.withPayload(payload)
|
||
.setHeader("mqtt_topic", topic)
|
||
.setHeader("mqtt_qos", defaultQos)
|
||
.build());
|
||
}
|
||
|
||
/**
|
||
* 发送MQTT字节消息
|
||
*
|
||
* @param topic 主题
|
||
* @param payload 原始字节
|
||
*/
|
||
public void sendMessageBytes(String topic, byte[] payload) {
|
||
log.info("发送MQTT字节消息 -> 主题: {}, QOS: {}, 长度: {}, HEX: {}", topic, defaultQos, payload.length, toHex(payload));
|
||
mqttOutboundChannel.send(MessageBuilder.withPayload(payload)
|
||
.setHeader("mqtt_topic", topic)
|
||
.setHeader("mqtt_qos", defaultQos)
|
||
.build());
|
||
}
|
||
|
||
private static String toHex(byte[] bytes) {
|
||
StringBuilder sb = new StringBuilder();
|
||
for (byte b : bytes) {
|
||
sb.append(String.format("%02X ", b));
|
||
}
|
||
return sb.toString().trim();
|
||
}
|
||
|
||
private static byte[] parseHex(String hex) {
|
||
String cleaned = hex.replaceAll("[^0-9A-Fa-f]", "");
|
||
if ((cleaned.length() % 2) != 0) {
|
||
throw new IllegalArgumentException("HEX长度不是偶数");
|
||
}
|
||
int len = cleaned.length() / 2;
|
||
byte[] out = new byte[len];
|
||
for (int i = 0; i < len; i++) {
|
||
int hi = Character.digit(cleaned.charAt(2 * i), 16);
|
||
int lo = Character.digit(cleaned.charAt(2 * i + 1), 16);
|
||
out[i] = (byte) ((hi << 4) | lo);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
private static byte computeBcc(byte[] buf, int startInclusive, int endExclusive) {
|
||
byte b = 0;
|
||
for (int i = startInclusive; i < endExclusive; i++) {
|
||
b ^= buf[i];
|
||
}
|
||
return b;
|
||
}
|
||
|
||
private static byte[] deriveKeyFromSn(String sn) {
|
||
byte[] key = new byte[16];
|
||
byte[] snBytes = sn == null ? new byte[0] : sn.getBytes(StandardCharsets.UTF_8);
|
||
int copy = Math.min(snBytes.length, 16);
|
||
System.arraycopy(snBytes, 0, key, 0, copy);
|
||
return key;
|
||
}
|
||
|
||
private static byte[] aesEcbPkcs5Decrypt(byte[] ciphertext, byte[] key) throws GeneralSecurityException {
|
||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||
SecretKeySpec spec = new SecretKeySpec(key, "AES");
|
||
cipher.init(Cipher.DECRYPT_MODE, spec);
|
||
return cipher.doFinal(ciphertext);
|
||
}
|
||
|
||
private static byte[] aesEcbPkcs5Encrypt(byte[] plaintext, byte[] key) throws GeneralSecurityException {
|
||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||
SecretKeySpec spec = new SecretKeySpec(key, "AES");
|
||
cipher.init(Cipher.ENCRYPT_MODE, spec);
|
||
return cipher.doFinal(plaintext);
|
||
}
|
||
|
||
/**
|
||
* 发送设备控制命令
|
||
*
|
||
* @param deviceCode 设备编号
|
||
* @param command 控制命令
|
||
*/
|
||
public void sendDeviceCommand(String deviceCode, String command) {
|
||
Map<String, Object> commandData = new HashMap<>();
|
||
commandData.put("deviceCode", deviceCode);
|
||
commandData.put("command", command);
|
||
commandData.put("timestamp", new Date());
|
||
|
||
String topic = "evobms/" + deviceCode + "/command";
|
||
String payload = JSON.toJSONString(commandData);
|
||
|
||
sendMessage(topic, payload);
|
||
}
|
||
|
||
/**
|
||
* 处理接收到的设备数据
|
||
*
|
||
* @param topic 主题
|
||
* @param message 消息内容
|
||
*/
|
||
public void handleDeviceData(String topic, byte[] message) {
|
||
try {
|
||
// 从主题提取设备唯一编号(紧跟在"bbox"后面的段)
|
||
String[] topicParts = topic.split("/");
|
||
String deviceCode = null;
|
||
for (int i = 0; i < topicParts.length - 1; i++) {
|
||
if ("bbox".equalsIgnoreCase(topicParts[i])) {
|
||
deviceCode = topicParts[i + 1];
|
||
break;
|
||
}
|
||
}
|
||
if (deviceCode == null) {
|
||
// 兜底:考虑有无前导斜杠
|
||
deviceCode = topicParts[topic.startsWith("/") ? 2 : 1];
|
||
}
|
||
|
||
log.info("处理设备数据 -> 设备: {}, 主题: {}, 长度: {}", deviceCode, topic, message == null ? 0 : message.length);
|
||
|
||
// 根据设备唯一编号查询系统表的SN码;查询不到则回退使用deviceCode
|
||
String deviceSn = lookupDeviceSnByCode(deviceCode);
|
||
if (deviceSn == null || deviceSn.isEmpty()) {
|
||
deviceSn = deviceCode;
|
||
}
|
||
|
||
// 直接解析GB/T 32960二进制帧
|
||
String data = parseGBT32960Data(message, deviceSn);
|
||
// 逐字段解析返回的data字符串
|
||
parseDataFields(data, deviceSn);
|
||
} catch (Exception e) {
|
||
log.error("处理设备数据时发生错误: {}", e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 逐字段解析数据字符串 - 直接解析十六进制数据
|
||
*
|
||
* @param data 十六进制数据字符串
|
||
* @param deviceSn 设备SN码
|
||
*/
|
||
private void parseDataFields(String data, String deviceSn) {
|
||
if (data == null || data.isEmpty()) {
|
||
log.warn("设备 {}: 无有效数据可解析", deviceSn);
|
||
return;
|
||
}
|
||
try {
|
||
log.info("设备 {} 原始数据: {}", deviceSn, data);
|
||
|
||
// 清理数据,移除非十六进制字符
|
||
String cleanData = data.replaceAll("[^0-9A-Fa-f]", "");
|
||
|
||
if (cleanData.length() < 4) {
|
||
log.warn("设备 {}: 数据长度不足", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 将十六进制字符串转换为字节数组
|
||
byte[] message = hexStringToByteArray(cleanData);
|
||
|
||
// 按照协议索引逐段解析
|
||
parseByProtocolIndex(message, deviceSn);
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析数据字段时发生错误: {}", deviceSn, e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 按照协议定义的索引位置解析各个数据段
|
||
*/
|
||
private void parseByProtocolIndex(byte[] message, String deviceSn) {
|
||
try {
|
||
// 1. 时间信息 - 索引24-29 (6字节)
|
||
Date frameTime = parseTimeAtIndex(message, 0, deviceSn);
|
||
if (frameTime == null) {
|
||
frameTime = new Date();
|
||
}
|
||
|
||
// 2. 整车数据 - 索引30-49 (20字节)
|
||
parseVehicleDataAtIndex(message, 6, deviceSn, frameTime);
|
||
|
||
// 3. 位置信息 - 索引51-60 (10字节)
|
||
parseLocationDataAtIndex(message, 27, deviceSn, frameTime);
|
||
|
||
// 4. 极值数据 - 索引61-75 (14字节)
|
||
parseExtremeDataAtIndex(message, 37, deviceSn, frameTime);
|
||
|
||
// 5. 单体电压数据 - 索引76-447 (372字节)
|
||
parseCellVoltageAtIndex(message, 52, deviceSn, frameTime);
|
||
|
||
// 6. 单体温度数据 - 索引448-542 (95字节)
|
||
parseCellTemperatureAtIndex(message, 424, deviceSn, frameTime);
|
||
|
||
// 7. 累计充放电量 - 索引543-553 (11字节)
|
||
parseChargeDischargeAtIndex(message, 519, deviceSn, frameTime);
|
||
|
||
log.info("设备 {} 所有数据段解析完成", deviceSn);
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 按索引解析时发生错误: {}", deviceSn, e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析时间信息 - 索引24-29 (6字节)
|
||
* 格式: 年(偏移2000) + 月 + 日 + 时 + 分 + 秒
|
||
*/
|
||
private Date parseTimeAtIndex(byte[] message, int startIndex, String deviceSn) {
|
||
try {
|
||
if (message.length < startIndex + 6) {
|
||
log.warn("设备 {}: 时间数据索引越界", deviceSn);
|
||
return null;
|
||
}
|
||
|
||
int year = (message[startIndex] & 0xFF) + 2000; // 索引24: 年
|
||
int month = message[startIndex + 1] & 0xFF; // 索引25: 月
|
||
int day = message[startIndex + 2] & 0xFF; // 索引26: 日
|
||
int hour = message[startIndex + 3] & 0xFF; // 索引27: 时
|
||
int minute = message[startIndex + 4] & 0xFF; // 索引28: 分
|
||
int second = message[startIndex + 5] & 0xFF; // 索引29: 秒
|
||
|
||
if (isValidTime(year, month, day, hour, minute, second)) {
|
||
String time = String.format("%04d-%02d-%02d %02d:%02d:%02d",
|
||
year, month, day, hour, minute, second);
|
||
log.info("设备 {} - 时间: {}", deviceSn, time);
|
||
saveTimeData(deviceSn, time);
|
||
Calendar cal = Calendar.getInstance(TimeZone.getDefault());
|
||
cal.set(year, month - 1, day, hour, minute, second);
|
||
cal.set(Calendar.MILLISECOND, 0);
|
||
return cal.getTime();
|
||
} else {
|
||
log.info("设备 {} - 时间: 无效数据({}-{}-{} {}:{}:{})",
|
||
deviceSn, year, month, day, hour, minute, second);
|
||
return null;
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析时间数据时发生错误: {}", deviceSn, e.getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析整车数据 - 索引30-49 (20字节)
|
||
*/
|
||
private void parseVehicleDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
|
||
try {
|
||
if (message.length < startIndex + 20) {
|
||
log.warn("设备 {}: 整车数据索引越界", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 索引30: 数据标识 (应该是0x01)
|
||
byte dataFlag = message[startIndex];
|
||
if (dataFlag != 0x01) {
|
||
log.warn("设备 {}: 整车数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
|
||
return;
|
||
}
|
||
|
||
log.info("设备 {} - 发现整车数据", deviceSn);
|
||
|
||
// 索引32: 充电状态
|
||
byte chargeStatus = message[startIndex + 2];
|
||
log.info("设备 {} - 充电状态: 0x{} ({})", deviceSn,
|
||
String.format("%02X", chargeStatus), getChargeStatusDescription(chargeStatus));
|
||
|
||
// 索引40-41: 总电压 (0.1V)
|
||
int totalVoltage = ((message[startIndex + 10] & 0xFF) << 8) | (message[startIndex + 11] & 0xFF);
|
||
double voltageValue = totalVoltage * 0.1;
|
||
log.info("设备 {} - 总电压: {} V", deviceSn, voltageValue);
|
||
|
||
// 索引42-43: 总电流 (0.1A, 偏移10000)
|
||
int totalCurrent = ((message[startIndex + 12] & 0xFF) << 8) | (message[startIndex + 13] & 0xFF);
|
||
double currentValue = (totalCurrent - 10000) * 0.1;
|
||
log.info("设备 {} - 总电流: {} A", deviceSn, currentValue);
|
||
|
||
// 索引44: SOC (%)
|
||
int soc = message[startIndex + 14] & 0xFF;
|
||
log.info("设备 {} - SOC: {} %", deviceSn, soc);
|
||
|
||
// 索引47-48: 正极绝缘电阻 (Ω)
|
||
int insulationResistance = ((message[startIndex + 17] & 0xFF) << 8) | (message[startIndex + 18] & 0xFF);
|
||
log.info("设备 {} - 正极绝缘电阻: {} Ω", deviceSn, insulationResistance);
|
||
|
||
// 保存整车数据
|
||
saveVehicleData(deviceSn, voltageValue, currentValue, soc, chargeStatus, insulationResistance, frameTime);
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析整车数据时发生错误: {}", deviceSn, e.getMessage());
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 解析位置信息 - 索引51-60 (10字节)
|
||
*/
|
||
private void parseLocationDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
|
||
try {
|
||
if (message.length < startIndex + 10) {
|
||
log.warn("设备 {}: 位置数据索引越界", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 索引51: 数据标识 (应该是0x05)
|
||
byte dataFlag = message[startIndex];
|
||
if (dataFlag != 0x05) {
|
||
log.warn("设备 {}: 位置数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
|
||
return;
|
||
}
|
||
|
||
log.info("设备 {} - 发现位置信息", deviceSn);
|
||
|
||
// 索引52: 位置状态
|
||
byte locationStatus = message[startIndex + 1];
|
||
// 转成十六进制字符串,例如 "00", "01"
|
||
String locationStatusHex = String.format("%02X", locationStatus);
|
||
// 索引53-56: 经度 (0.000001°)
|
||
long longitude = ((message[startIndex + 2] & 0xFFL) << 24) |
|
||
((message[startIndex + 3] & 0xFFL) << 16) |
|
||
((message[startIndex + 4] & 0xFFL) << 8) |
|
||
(message[startIndex + 5] & 0xFFL);
|
||
|
||
// 索引57-60: 纬度 (0.000001°)
|
||
long latitude = ((message[startIndex + 6] & 0xFFL) << 24) |
|
||
((message[startIndex + 7] & 0xFFL) << 16) |
|
||
((message[startIndex + 8] & 0xFFL) << 8) |
|
||
(message[startIndex + 9] & 0xFFL);
|
||
|
||
if (longitude != 0 || latitude != 0) {
|
||
double lon = longitude / 1000000.0;
|
||
double lat = latitude / 1000000.0;
|
||
|
||
log.info("设备 {} - 位置: 经度={}°, 纬度={}° (状态:0x{})", deviceSn, lon, lat, locationStatusHex);
|
||
processLocationData(deviceSn, lon, lat, locationStatusHex, frameTime);
|
||
} else {
|
||
log.info("设备 {} - 位置: 无效数据", deviceSn);
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析位置数据时发生错误: {}", deviceSn, e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析极值数据 - 索引61-75 (14字节)
|
||
*/
|
||
private void parseExtremeDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
|
||
try {
|
||
if (message.length < startIndex + 14) {
|
||
log.warn("设备 {}: 极值数据索引越界", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 索引61: 数据标识 (应该是0x06)
|
||
byte dataFlag = message[startIndex];
|
||
if (dataFlag != 0x06) {
|
||
log.warn("设备 {}: 极值数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
|
||
return;
|
||
}
|
||
|
||
log.info("设备 {} - 发现极值数据", deviceSn);
|
||
|
||
// 索引62: 最高电压子系统号
|
||
byte maxVoltageSystem = message[startIndex + 1];
|
||
// 索引63: 最高电压探针号
|
||
byte maxVoltageProbe = message[startIndex + 2];
|
||
// 索引64-65: 最高单体电压 (mV)
|
||
double maxVoltage = ((message[startIndex + 3] & 0xFF) << 8) | (message[startIndex + 4] & 0xFF);
|
||
double maxVoltageValue = maxVoltage * 0.001; // 转换为V
|
||
log.info("设备 {} - 最高电压: 子系统{}-探针{}, {}V", deviceSn, maxVoltageSystem, maxVoltageProbe, maxVoltageValue);
|
||
|
||
// 索引66: 最低电压子系统号
|
||
byte minVoltageSystem = message[startIndex + 5];
|
||
// 索引67: 最低电压探针号
|
||
byte minVoltageProbe = message[startIndex + 6];
|
||
// 索引68-69: 最低单体电压 (mV)
|
||
double minVoltage = ((message[startIndex + 7] & 0xFF) << 8) | (message[startIndex + 8] & 0xFF);
|
||
double minVoltageValue = minVoltage * 0.001; // 转换为V
|
||
log.info("设备 {} - 最低电压: 子系统{}-探针{}, {}V", deviceSn, minVoltageSystem, minVoltageProbe, minVoltageValue);
|
||
// 索引70: 最高温度子系统号
|
||
byte maxTempSystem = message[startIndex + 9];
|
||
// 索引71: 最高温度探针号
|
||
byte maxTempProbe = message[startIndex + 10];
|
||
// 索引72: 最高单体温度 (偏移40°C)
|
||
int maxTemperature = message[startIndex + 11] & 0xFF;
|
||
int maxTempValue = maxTemperature - 40;
|
||
log.info("设备 {} - 最高温度: 子系统{}-探针{}, {}°C", deviceSn, maxTempSystem, maxTempProbe, maxTempValue);
|
||
// 索引73: 最低温度子系统号
|
||
byte minTempSystem = message[startIndex + 12];
|
||
// 索引74: 最低温度探针号
|
||
byte minTempProbe = message[startIndex + 13];
|
||
// 索引75: 最低单体温度 (偏移40°C)
|
||
int minTemperature = message[startIndex + 14] & 0xFF;
|
||
int minTempValue = minTemperature - 40;
|
||
log.info("设备 {} - 最低温度: 子系统{}-探针{}, {}°C", deviceSn, minTempSystem, minTempProbe, minTempValue);
|
||
// 保存极值数据
|
||
saveExtremeData(deviceSn, (maxVoltageSystem & 0xFF), (maxVoltageProbe & 0xFF), maxVoltageValue, (minVoltageSystem & 0xFF), (minVoltageProbe & 0xFF), minVoltageValue, (maxTempSystem & 0xFF), (maxTempProbe & 0xFF), maxTempValue, (minTempSystem & 0xFF), (minTempProbe & 0xFF), minTempValue, frameTime);
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析极值数据时发生错误: {}", deviceSn, e.getMessage());
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 解析单体电压数据 - 索引76-447 (372字节)
|
||
*/
|
||
private void parseCellVoltageAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
|
||
try {
|
||
if (message.length < startIndex + 372) {
|
||
log.warn("设备 {}: 单体电压数据索引越界", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 索引76: 数据标识 (应该是0x08)
|
||
byte dataFlag = message[startIndex];
|
||
if (dataFlag != 0x08) {
|
||
log.warn("设备 {}: 单体电压数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
|
||
return;
|
||
}
|
||
|
||
log.info("设备 {} - 发现单体电压数据", deviceSn);
|
||
|
||
// 索引77: 可充电储能子系统个数
|
||
byte subsystemCount = message[startIndex + 1];
|
||
// 索引78: 可充电储能子系统号
|
||
byte subsystemNum = message[startIndex + 2];
|
||
|
||
// 索引79-80: 可充电储能装置总压 (0.1V)
|
||
int storageVoltage = ((message[startIndex + 3] & 0xFF) << 8) | (message[startIndex + 4] & 0xFF);
|
||
double storageVoltageValue = storageVoltage * 0.1;
|
||
|
||
// 索引81-82: 可充电储能装置总流 (0.1A, 偏移10000)
|
||
int storageCurrent = ((message[startIndex + 5] & 0xFF) << 8) | (message[startIndex + 6] & 0xFF);
|
||
double storageCurrentValue = (storageCurrent - 10000) * 0.1;
|
||
|
||
log.info("设备 {} - 储能装置: 子系统{}/{}, 电压{}V, 电流{}A", deviceSn,
|
||
subsystemNum, subsystemCount, storageVoltageValue, storageCurrentValue);
|
||
|
||
// 索引83-84: 单体电压总数
|
||
int totalCellCount = ((message[startIndex + 7] & 0xFF) << 8) | (message[startIndex + 8] & 0xFF);
|
||
// 索引85-86: 本帧起始电芯序号
|
||
int startCellIndex = ((message[startIndex + 9] & 0xFF) << 8) | (message[startIndex + 10] & 0xFF);
|
||
// 索引87: 本帧电芯总数
|
||
int frameCellCount = message[startIndex + 11] & 0xFF;
|
||
|
||
log.info("设备 {} - 电芯信息: 总数{}个, 本帧{}-{}", deviceSn,
|
||
totalCellCount, startCellIndex, startCellIndex + frameCellCount - 1);
|
||
|
||
// 索引88开始: 单体电压数据 (每个电芯2字节)
|
||
int voltageDataStart = startIndex + 12;
|
||
boolean hasValidVoltage = checkCellVoltagesValid(message, voltageDataStart, frameCellCount);
|
||
|
||
if (hasValidVoltage) {
|
||
log.info("设备 {} - 发现有效单体电压数据", deviceSn);
|
||
List<Double> voltages = new ArrayList<>();
|
||
for (int i = 0; i < frameCellCount; i++) {
|
||
int v = ((message[voltageDataStart + i * 2] & 0xFF) << 8) | (message[voltageDataStart + i * 2 + 1] & 0xFF);
|
||
double vv = v * 0.001;
|
||
voltages.add(vv);
|
||
}
|
||
SubsystemVoltage sv = new SubsystemVoltage();
|
||
sv.setDeviceId(deviceSn);
|
||
sv.setTimestamp(frameTime);
|
||
sv.setSubsystemCount((int) subsystemCount);
|
||
sv.setSubsystemNo((int) subsystemNum);
|
||
sv.setSubsystemVoltage(BigDecimal.valueOf(storageVoltageValue));
|
||
sv.setSubsystemCurrent(BigDecimal.valueOf(storageCurrentValue));
|
||
sv.setTotalBatteryCount(totalCellCount);
|
||
sv.setFrameStartBatteryNo(startCellIndex);
|
||
sv.setFrameBatteryCount(frameCellCount);
|
||
sv.setCreateTime(new Date());
|
||
sv.setCreateBy(deviceSn);
|
||
String voltagesStr = voltages.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(","));
|
||
sv.setBatteryVoltages(JSON.toJSONString(voltagesStr));
|
||
subsystemVoltageService.insertSubsystemVoltage(sv);
|
||
parseCellVoltagesDetail(message, voltageDataStart, deviceSn, startCellIndex, frameCellCount);
|
||
} else {
|
||
log.info("设备 {} - 单体电压数据: 全部为0V", deviceSn);
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析单体电压数据时发生错误: {}", deviceSn, e.getMessage());
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 解析单体温度数据 - 索引448-542 (95字节)
|
||
*/
|
||
private void parseCellTemperatureAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
|
||
try {
|
||
if (message.length < startIndex + 89) {
|
||
log.warn("设备 {}: 单体温度数据索引越界", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 索引448: 数据标识 (应该是0x09)
|
||
byte dataFlag = message[startIndex];
|
||
if (dataFlag != 0x09) {
|
||
log.warn("设备 {}: 单体温度数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
|
||
return;
|
||
}
|
||
|
||
log.info("设备 {} - 发现单体温度数据", deviceSn);
|
||
|
||
// 索引449: 可充电储能子系统个数
|
||
byte subsystemCount = message[startIndex + 1];
|
||
// 索引450: 可充电储能子系统号
|
||
byte subsystemNum = message[startIndex + 2];
|
||
|
||
// 索引451-452: 单体温度总数
|
||
int totalTempCount = ((message[startIndex + 3] & 0xFF) << 8) | (message[startIndex + 4] & 0xFF);
|
||
|
||
log.info("设备 {} - 温度探头: 子系统{}/{}, 总数{}个", deviceSn,
|
||
subsystemNum, subsystemCount, totalTempCount);
|
||
|
||
// 索引453开始: 单体温度数据 (每个探头1字节)
|
||
int tempDataStart = startIndex + 5;
|
||
boolean hasValidTemperature = checkCellTemperaturesValid(message, tempDataStart, totalTempCount);
|
||
|
||
if (hasValidTemperature) {
|
||
log.info("设备 {} - 发现有效单体温度数据", deviceSn);
|
||
List<Integer> temperatures = new ArrayList<>();
|
||
for (int i = 0; i < totalTempCount; i++) {
|
||
int t = message[tempDataStart + i] & 0xFF;
|
||
int tv = t - 40;
|
||
temperatures.add(tv);
|
||
}
|
||
SubsystemTemperature st = new SubsystemTemperature();
|
||
st.setDeviceId(deviceSn);
|
||
st.setTimestamp(frameTime);
|
||
st.setSubsystemCount((int) subsystemCount);
|
||
st.setSubsystemNo((int) subsystemNum);
|
||
st.setTempProbeCount(totalTempCount);
|
||
st.setCreateTime(new Date());
|
||
st.setCreateBy(deviceSn);
|
||
String tempsStr = temperatures.stream().map(Object::toString).collect(Collectors.joining(","));
|
||
st.setTemperatureValues(JSON.toJSONString(tempsStr));
|
||
subsystemTemperatureService.insertSubsystemTemperature(st);
|
||
parseCellTemperaturesDetail(message, tempDataStart, deviceSn, totalTempCount);
|
||
} else {
|
||
log.info("设备 {} - 单体温度数据: 全部为无效值", deviceSn);
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析单体温度数据时发生错误: {}", deviceSn, e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析累计充放电量 - 索引543-553 (11字节)
|
||
*/
|
||
private void parseChargeDischargeAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
|
||
try {
|
||
if (message.length < startIndex + 11) {
|
||
log.warn("设备 {}: 充放电数据索引越界", deviceSn);
|
||
return;
|
||
}
|
||
|
||
// 索引543: 数据标识 (应该是0x80)
|
||
byte dataFlag = message[startIndex];
|
||
if (dataFlag != (byte) 0x80) {
|
||
log.warn("设备 {}: 充放电数据标识错误: 0x{}", deviceSn, String.format("%02X", dataFlag));
|
||
return;
|
||
}
|
||
|
||
log.info("设备 {} - 发现累计充放电数据", deviceSn);
|
||
|
||
// 跳过索引544-545的保留字节
|
||
// 索引546-549: 累计放电电量 (0.1kWh)
|
||
long dischargeEnergy = ((message[startIndex + 3] & 0xFFL) << 24) |
|
||
((message[startIndex + 4] & 0xFFL) << 16) |
|
||
((message[startIndex + 5] & 0xFFL) << 8) |
|
||
(message[startIndex + 6] & 0xFFL);
|
||
|
||
// 索引550-553: 累计充电电量 (0.1kWh)
|
||
long chargeEnergy = ((message[startIndex + 7] & 0xFFL) << 24) |
|
||
((message[startIndex + 8] & 0xFFL) << 16) |
|
||
((message[startIndex + 9] & 0xFFL) << 8) |
|
||
(message[startIndex + 10] & 0xFFL);
|
||
|
||
double dischargeKwh = dischargeEnergy * 0.1;
|
||
double chargeKwh = chargeEnergy * 0.1;
|
||
|
||
log.info("设备 {} - 累计充放电量: 放电{}kWh, 充电{}kWh", deviceSn, dischargeKwh, chargeKwh);
|
||
|
||
saveChargeDischargeSummary(deviceSn, dischargeKwh, chargeKwh, frameTime);
|
||
|
||
} catch (Exception e) {
|
||
log.error("设备 {} 解析充放电数据时发生错误: {}", deviceSn, e.getMessage());
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 解析详细的单体温度数据
|
||
*/
|
||
private void parseCellTemperaturesDetail(byte[] message, int startIndex, String deviceSn, int tempCount) {
|
||
List<Integer> temperatures = new ArrayList<>();
|
||
|
||
for (int i = 0; i < tempCount; i++) {
|
||
int temperature = message[startIndex + i] & 0xFF;
|
||
int tempValue = temperature - 40; // 温度偏移量
|
||
temperatures.add(tempValue);
|
||
|
||
// 记录前几个温度用于调试
|
||
if (i < 5) {
|
||
log.debug("设备 {} - 温度探头{}: {}°C", deviceSn, i + 1, tempValue);
|
||
}
|
||
}
|
||
|
||
// 计算统计信息
|
||
int minTemp = temperatures.stream().mapToInt(v -> v).min().orElse(0);
|
||
int maxTemp = temperatures.stream().mapToInt(v -> v).max().orElse(0);
|
||
double avgTemp = temperatures.stream().mapToInt(v -> v).average().orElse(0);
|
||
|
||
log.info("设备 {} - 温度统计: 最小值{}°C, 最大值{}°C, 平均值{}°C",
|
||
deviceSn, minTemp, maxTemp, avgTemp);
|
||
}
|
||
|
||
|
||
/**
|
||
* 检查单体温度数据有效性
|
||
*/
|
||
private boolean checkCellTemperaturesValid(byte[] message, int startIndex, int tempCount) {
|
||
for (int i = 0; i < tempCount; i++) {
|
||
int temperature = message[startIndex + i] & 0xFF;
|
||
if (temperature != 0 && temperature != 0xFF) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 解析详细的单体电压数据
|
||
*/
|
||
private void parseCellVoltagesDetail(byte[] message, int startIndex, String deviceSn, int startCellIndex, int cellCount) {
|
||
List<Double> voltages = new ArrayList<>();
|
||
|
||
for (int i = 0; i < cellCount; i++) {
|
||
int voltage = ((message[startIndex + i * 2] & 0xFF) << 8) |
|
||
(message[startIndex + i * 2 + 1] & 0xFF);
|
||
double voltageValue = voltage * 0.001; // mV转V
|
||
voltages.add(voltageValue);
|
||
|
||
// 记录前几个电芯电压用于调试
|
||
if (i < 5) {
|
||
log.debug("设备 {} - 电芯{}电压: {}V", deviceSn, startCellIndex + i, voltageValue);
|
||
}
|
||
}
|
||
|
||
// 计算统计信息
|
||
double minVoltage = voltages.stream().mapToDouble(v -> v).min().orElse(0);
|
||
double maxVoltage = voltages.stream().mapToDouble(v -> v).max().orElse(0);
|
||
double avgVoltage = voltages.stream().mapToDouble(v -> v).average().orElse(0);
|
||
|
||
log.info("设备 {} - 电压统计: 最小值{}V, 最大值{}V, 平均值{}V", deviceSn, minVoltage, maxVoltage, avgVoltage);
|
||
}
|
||
|
||
/**
|
||
* 检查单体电压数据有效性
|
||
*/
|
||
private boolean checkCellVoltagesValid(byte[] message, int startIndex, int cellCount) {
|
||
for (int i = 0; i < cellCount * 2; i += 2) {
|
||
int voltage = ((message[startIndex + i] & 0xFF) << 8) | (message[startIndex + i + 1] & 0xFF);
|
||
if (voltage > 0) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 充电状态描述
|
||
*/
|
||
private int getChargeStatusDescription(byte status) {
|
||
/** 充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) */
|
||
switch (status & 0xFF) {
|
||
case 0x01:
|
||
return 1;
|
||
case 0x02:
|
||
return 2;
|
||
case 0x03:
|
||
return 3;
|
||
case 0x04:
|
||
return 4;
|
||
case 0xFE:
|
||
return 254;
|
||
case 0xFF:
|
||
return 255;
|
||
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* 检查时间有效性
|
||
*/
|
||
private boolean isValidTime(int year, int month, int day, int hour, int minute, int second) {
|
||
return year >= 2000 && year <= 2100 &&
|
||
month >= 1 && month <= 12 &&
|
||
day >= 1 && day <= 31 &&
|
||
hour >= 0 && hour <= 23 &&
|
||
minute >= 0 && minute <= 59 &&
|
||
second >= 0 && second <= 59;
|
||
}
|
||
|
||
/**
|
||
* 十六进制字符串转字节数组
|
||
*/
|
||
private byte[] hexStringToByteArray(String hex) {
|
||
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;
|
||
}
|
||
|
||
|
||
/**
|
||
* 保存单体电压数据
|
||
*/
|
||
private void saveCellVoltageData(String deviceSn, byte subsystemNum, int startIndex, int count, List<Double> voltages, double min, double max, double avg) {
|
||
// TODO: 实现单体电压数据保存逻辑
|
||
log.debug("保存设备 {} 子系统 {} 电压数据: 起始{}, 数量{}, 范围{}-{}V", deviceSn, subsystemNum, startIndex, count, min, max);
|
||
}
|
||
|
||
/**
|
||
* 保存单体温度数据
|
||
*/
|
||
private void saveCellTemperatureData(String deviceSn, byte subsystemNum, int count, List<Integer> temperatures, int min, int max, double avg) {
|
||
// TODO: 实现单体温度数据保存逻辑
|
||
log.debug("保存设备 {} 子系统 {} 温度数据: 数量{}, 范围{}-{}°C", deviceSn, subsystemNum, count, min, max);
|
||
}
|
||
|
||
/**
|
||
* 保存时间数据
|
||
*/
|
||
private void saveTimeData(String deviceSn, String time) {
|
||
// TODO: 实现时间数据保存逻辑
|
||
log.debug("保存设备 {} 时间数据: {}", deviceSn, time);
|
||
}
|
||
|
||
/**
|
||
* 保存整车数据
|
||
*/
|
||
private void saveVehicleData(String deviceSn, double voltage, double current, int soc, byte chargeStatus,int insulationResistance, Date frameTime) {
|
||
// 保存整车数据到数据库
|
||
VehicleData ve = new VehicleData();
|
||
ve.setDeviceId(deviceSn);
|
||
ve.setTimestamp(frameTime);
|
||
ve.setTotalVoltage(BigDecimal.valueOf(voltage));
|
||
ve.setTotalCurrent(BigDecimal.valueOf(current));
|
||
ve.setSoc(soc);
|
||
ve.setChargingStatus(getChargeStatusDescription(chargeStatus));
|
||
ve.setInsulationResistance(insulationResistance);
|
||
ve.setCreateTime(new Date());
|
||
ve.setCreateBy(deviceSn);
|
||
vehicleDataService.insertVehicleData(ve);
|
||
}
|
||
|
||
|
||
private void processLocationData(String deviceSn, double longitude, double latitude,String locationStatus, Date frameTime) {
|
||
VehicleLocation ve = new VehicleLocation();
|
||
ve.setDeviceId(deviceSn);
|
||
ve.setTimestamp(frameTime);
|
||
// 十六进制字符串(如 "00")转成整数(如 0)
|
||
int status = Integer.parseInt(locationStatus, 16);
|
||
ve.setPositioningStatus(status);
|
||
ve.setLongitude(BigDecimal.valueOf(longitude));
|
||
ve.setLatitude(BigDecimal.valueOf(latitude));
|
||
ve.setCreateTime(new Date());
|
||
ve.setCreateBy(deviceSn);
|
||
vehicleLocationService.insertVehicleLocation(ve);
|
||
}
|
||
|
||
private void saveExtremeData(String deviceSn, int maxVoltageSubsystemNo, int maxVoltageBatteryNo, double maxVoltageValue, int minVoltageSubsystemNo, int minVoltageBatteryNo, double minVoltageValue, int maxTempSubsystemNo,
|
||
int maxTempProbeNo, int maxTempValue, int minTempSubsystemNo, int minTempProbeNo, int minTempValue, Date frameTime) {
|
||
ExtremeValues ex = new ExtremeValues();
|
||
ex.setDeviceId(deviceSn);
|
||
ex.setTimestamp(frameTime);
|
||
ex.setMaxVoltageSubsystemNo(maxVoltageSubsystemNo);
|
||
ex.setMaxVoltageBatteryNo(maxVoltageBatteryNo);
|
||
ex.setMaxVoltageValue(BigDecimal.valueOf(maxVoltageValue));
|
||
ex.setMinVoltageSubsystemNo(minVoltageSubsystemNo);
|
||
ex.setMinVoltageBatteryNo(minVoltageBatteryNo);
|
||
ex.setMinVoltageValue(BigDecimal.valueOf(minVoltageValue));
|
||
ex.setMaxTempSubsystemNo(maxTempSubsystemNo);
|
||
ex.setMaxTempProbeNo(maxTempProbeNo);
|
||
ex.setMaxTempValue(BigDecimal.valueOf(maxTempValue));
|
||
ex.setMinTempSubsystemNo(minTempSubsystemNo);
|
||
ex.setMinTempProbeNo(minTempProbeNo);
|
||
ex.setMinTempValue(BigDecimal.valueOf(minTempValue));
|
||
extremeValuesService.insertExtremeValues(ex);
|
||
}
|
||
|
||
private void saveChargeDischargeSummary(String deviceSn, double dischargeKwh, double chargeKwh, Date frameTime) {
|
||
ChargeDischargeSummary s = new ChargeDischargeSummary();
|
||
s.setDeviceId(deviceSn);
|
||
s.setTimestamp(frameTime);
|
||
s.setDischargeKwh(BigDecimal.valueOf(dischargeKwh));
|
||
s.setChargeKwh(BigDecimal.valueOf(chargeKwh));
|
||
s.setCreateTime(new Date());
|
||
s.setCreateBy(deviceSn);
|
||
chargeDischargeSummaryService.insertChargeDischargeSummary(s);
|
||
}
|
||
|
||
/**
|
||
* 处理单体温度数据
|
||
*/
|
||
private void processCellTemperatureData(String deviceSn, Map<String, String> fieldMap) {
|
||
// TODO: 如果有详细的单体温度数据,在这里处理
|
||
}
|
||
|
||
/**
|
||
* 保存充放电数据
|
||
*/
|
||
private void saveChargeDischargeData(String deviceSn, String dischargeEnergy, String chargeEnergy, String time) {
|
||
// TODO: 实现充放电数据保存逻辑
|
||
}
|
||
|
||
|
||
/**
|
||
* 更新设备数据
|
||
*
|
||
* @param device 设备对象
|
||
* @param data 数据
|
||
*/
|
||
private void updateDeviceData(BmsDevice device, Map<String, Object> data) {
|
||
if (data.containsKey("voltage")) {
|
||
device.setCurrentVoltage(Double.valueOf(data.get("voltage").toString()));
|
||
}
|
||
if (data.containsKey("current")) {
|
||
device.setCurrentCurrent(Double.valueOf(data.get("current").toString()));
|
||
}
|
||
if (data.containsKey("capacity")) {
|
||
device.setRemainingCapacity(Double.valueOf(data.get("capacity").toString()));
|
||
}
|
||
if (data.containsKey("temperature")) {
|
||
device.setTemperature(Double.valueOf(data.get("temperature").toString()));
|
||
}
|
||
|
||
device.setLastCommTime(new Date());
|
||
}
|
||
|
||
/**
|
||
* 发送设备状态查询命令
|
||
*
|
||
* @param deviceCode 设备编号
|
||
*/
|
||
public void queryDeviceStatus(String deviceCode) {
|
||
sendDeviceCommand(deviceCode, "STATUS_QUERY");
|
||
}
|
||
|
||
|
||
/**
|
||
* 根据设备唯一编号(deviceCode)查询系统设备表的SN码
|
||
*/
|
||
private String lookupDeviceSnByCode(String deviceCode) {
|
||
try {
|
||
BmsDevices query = new BmsDevices();
|
||
query.setDeviceId(deviceCode);
|
||
List<BmsDevices> list = bmsDevicesService.selectBmsDevicesList(query);
|
||
if (list != null && !list.isEmpty()) {
|
||
String sn = list.get(0).getDeviceSn();
|
||
if (sn != null && !sn.isEmpty()) {
|
||
return sn;
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("查询设备SN失败: {}", e.getMessage());
|
||
}
|
||
return null;
|
||
}
|
||
}
|