车辆位置,极值,位置等重要数据数据

This commit is contained in:
tzy 2026-01-08 17:35:41 +08:00
parent b29f7cfb81
commit 0de7503010
13 changed files with 265 additions and 302 deletions

11
pom.xml
View File

@ -49,6 +49,12 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- SpringBoot 测试 --> <!-- SpringBoot 测试 -->
<dependency> <dependency>
@ -270,6 +276,11 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies> </dependencies>

View File

@ -114,7 +114,8 @@ public class SecurityConfig
requests.antMatchers("/login", "/register", "/captchaImage").permitAll() requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源可匿名访问 // 静态资源可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/*/api-docs", "/druid/**", "/doc.html", "/doc.html/**", "/knife4j/**").permitAll()
.antMatchers("/dev-api/swagger-ui/**", "/dev-api/swagger-resources/**", "/dev-api/v3/api-docs/**", "/dev-api/doc.html", "/dev-api/knife4j/**").permitAll()
.antMatchers("/devices/**","/ota/**").permitAll() .antMatchers("/devices/**","/ota/**").permitAll()
.antMatchers("/battery-data/**").permitAll() .antMatchers("/battery-data/**").permitAll()
.antMatchers("/vehicle-data/**").permitAll() .antMatchers("/vehicle-data/**").permitAll()

View File

@ -54,11 +54,7 @@ public class SwaggerConfig
.apiInfo(apiInfo()) .apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示 // 设置哪些接口暴露给Swagger展示
.select() .select()
// 扫描所有有注解的api用这种方式更灵活 .apis(RequestHandlerSelectors.basePackage("com.evobms.project"))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 扫描指定包中的swagger注解
// .apis(RequestHandlerSelectors.basePackage("com.evobms.project.tool.swagger"))
// 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()) .paths(PathSelectors.any())
.build() .build()
/* 设置安全模式swagger可以设置访问token */ /* 设置安全模式swagger可以设置访问token */

View File

@ -2,6 +2,7 @@ package com.evobms.project.Battery.domain;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import com.evobms.framework.aspectj.lang.annotation.Excel; import com.evobms.framework.aspectj.lang.annotation.Excel;
@ -9,10 +10,11 @@ import com.evobms.framework.web.domain.BaseEntity;
/** /**
* 可充电储能子系统温度信息对象 subsystem_temperature * 可充电储能子系统温度信息对象 subsystem_temperature
* *
* @author 田志阳 * @author 田志阳
* @date 2025-11-15 * @date 2025-11-15
*/ */
@Data
public class SubsystemTemperature extends BaseEntity public class SubsystemTemperature extends BaseEntity
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -48,115 +50,11 @@ public class SubsystemTemperature extends BaseEntity
/** 创建时间 */ /** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date createdAt; private Date createTime;
/** 更新时间 */ /** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date updatedAt; private Date updateTime;
public void setId(Long id)
{
this.id = id;
}
public Long getId()
{
return id;
}
public void setDeviceId(String deviceId)
{
this.deviceId = deviceId;
}
public String getDeviceId()
{
return deviceId;
}
public void setTimestamp(Date timestamp)
{
this.timestamp = timestamp;
}
public Date getTimestamp()
{
return timestamp;
}
public void setSubsystemCount(Integer subsystemCount)
{
this.subsystemCount = subsystemCount;
}
public Integer getSubsystemCount()
{
return subsystemCount;
}
public void setSubsystemNo(Integer subsystemNo)
{
this.subsystemNo = subsystemNo;
}
public Integer getSubsystemNo()
{
return subsystemNo;
}
public void setTempProbeCount(Integer tempProbeCount)
{
this.tempProbeCount = tempProbeCount;
}
public Integer getTempProbeCount()
{
return tempProbeCount;
}
public void setTemperatureValues(String temperatureValues)
{
this.temperatureValues = temperatureValues;
}
public String getTemperatureValues()
{
return temperatureValues;
}
public void setCreatedAt(Date createdAt)
{
this.createdAt = createdAt;
}
public Date getCreatedAt()
{
return createdAt;
}
public void setUpdatedAt(Date updatedAt)
{
this.updatedAt = updatedAt;
}
public Date getUpdatedAt()
{
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("deviceId", getDeviceId())
.append("timestamp", getTimestamp())
.append("subsystemCount", getSubsystemCount())
.append("subsystemNo", getSubsystemNo())
.append("tempProbeCount", getTempProbeCount())
.append("temperatureValues", getTemperatureValues())
.append("createdAt", getCreatedAt())
.append("updatedAt", getUpdatedAt())
.toString();
}
} }

View File

@ -21,6 +21,8 @@ import com.evobms.project.Battery.service.IExtremeValuesService;
import com.evobms.project.Battery.domain.ExtremeValues; import com.evobms.project.Battery.domain.ExtremeValues;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/** /**
* 图标组件Controller * 图标组件Controller
* *
@ -28,6 +30,7 @@ import java.text.SimpleDateFormat;
* @date 2025-11-15 * @date 2025-11-15
*/ */
@RestController @RestController
@Api(tags = "图表组件")
public class BatteryChartController { public class BatteryChartController {
@Autowired @Autowired
@ -47,6 +50,7 @@ public class BatteryChartController {
// 图表数据按最近 hours 小时返回整车电压/电流/SOC 的时间序列 // 图表数据按最近 hours 小时返回整车电压/电流/SOC 的时间序列
@GetMapping("/battery-data/{deviceId}/chart") @GetMapping("/battery-data/{deviceId}/chart")
@ApiOperation(value = "整车电压电流SOC时间序列", notes = "按最近hours小时返回时间序列")
public List<ChartDataPoint> getChartData(@PathVariable("deviceId") String deviceId, @RequestParam(value = "hours", required = false, defaultValue = "24") int hours) { public List<ChartDataPoint> getChartData(@PathVariable("deviceId") String deviceId, @RequestParam(value = "hours", required = false, defaultValue = "24") int hours) {
long millis = System.currentTimeMillis() - (long) hours * 3600_000L; long millis = System.currentTimeMillis() - (long) hours * 3600_000L;
Date startTime = new Date(millis); Date startTime = new Date(millis);
@ -61,18 +65,21 @@ public class BatteryChartController {
// 整车最新返回 deviceId 最新的一条整车数据 // 整车最新返回 deviceId 最新的一条整车数据
@GetMapping("/vehicle-data/{deviceId}/latest") @GetMapping("/vehicle-data/{deviceId}/latest")
@ApiOperation(value = "整车最新数据", notes = "返回指定设备最新整车数据")
public VehicleData latestVehicleData(@PathVariable("deviceId") String deviceId) { public VehicleData latestVehicleData(@PathVariable("deviceId") String deviceId) {
return vehicleDataService.selectLatestByDeviceId(deviceId); return vehicleDataService.selectLatestByDeviceId(deviceId);
} }
// 位置最新返回 deviceId 最新的一条位置数据 // 位置最新返回 deviceId 最新的一条位置数据
@GetMapping("/vehicle-location/{deviceId}/latest") @GetMapping("/vehicle-location/{deviceId}/latest")
@ApiOperation(value = "车辆最新位置", notes = "返回指定设备最新位置数据")
public VehicleLocation latestVehicleLocation(@PathVariable("deviceId") String deviceId) { public VehicleLocation latestVehicleLocation(@PathVariable("deviceId") String deviceId) {
return vehicleLocationService.selectLatestByDeviceId(deviceId); return vehicleLocationService.selectLatestByDeviceId(deviceId);
} }
// 位置轨迹按时间范围返回位置列表start/end 为毫秒时间戳可选 limit 限制条数 // 位置轨迹按时间范围返回位置列表start/end 为毫秒时间戳可选 limit 限制条数
@GetMapping("/vehicle-location/{deviceId}") @GetMapping("/vehicle-location/{deviceId}")
@ApiOperation(value = "车辆位置轨迹", notes = "按时间范围返回位置列表")
public List<VehicleLocation> locationRange(@PathVariable("deviceId") String deviceId, public List<VehicleLocation> locationRange(@PathVariable("deviceId") String deviceId,
@RequestParam("start") long startMillis, @RequestParam("start") long startMillis,
@RequestParam("end") long endMillis, @RequestParam("end") long endMillis,
@ -84,25 +91,28 @@ public class BatteryChartController {
// 子系统最新帧电压返回指定子系统号最新的一帧电压数据 // 子系统最新帧电压返回指定子系统号最新的一帧电压数据
@GetMapping("/subsystem-voltage/{deviceId}/{subsystemNo}/latest") @GetMapping("/subsystem-voltage/{deviceId}/{subsystemNo}/latest")
@ApiOperation(value = "子系统最新电压帧", notes = "返回指定子系统最新电压数据")
public SubsystemVoltage latestVoltage(@PathVariable("deviceId") String deviceId, @PathVariable("subsystemNo") Integer subsystemNo) { public SubsystemVoltage latestVoltage(@PathVariable("deviceId") String deviceId, @PathVariable("subsystemNo") Integer subsystemNo) {
return subsystemVoltageService.selectLatestByDeviceAndSubsystem(deviceId, subsystemNo); return subsystemVoltageService.selectLatestByDeviceAndSubsystem(deviceId, subsystemNo);
} }
// 子系统最新帧温度返回指定子系统号最新的一帧温度数据 // 子系统最新帧温度返回指定子系统号最新的一帧温度数据
@GetMapping("/subsystem-temperature/{deviceId}/{subsystemNo}/latest") @GetMapping("/subsystem-temperature/{deviceId}/{subsystemNo}/latest")
@ApiOperation(value = "子系统最新温度帧", notes = "返回指定子系统最新温度数据")
public SubsystemTemperature latestTemperature(@PathVariable("deviceId") String deviceId, @PathVariable("subsystemNo") Integer subsystemNo) { public SubsystemTemperature latestTemperature(@PathVariable("deviceId") String deviceId, @PathVariable("subsystemNo") Integer subsystemNo) {
return subsystemTemperatureService.selectLatestByDeviceAndSubsystem(deviceId, subsystemNo); return subsystemTemperatureService.selectLatestByDeviceAndSubsystem(deviceId, subsystemNo);
} }
@GetMapping("/extreme-values/{deviceId}/latest") @GetMapping("/extreme-values/{deviceId}/latest")
@ApiOperation(value = "极值最新数据", notes = "返回指定设备极值数据支持before筛选")
public ExtremeValues latestExtremeValues(@PathVariable("deviceId") String deviceId, public ExtremeValues latestExtremeValues(@PathVariable("deviceId") String deviceId,
@RequestParam(value = "before", required = false) String before) { @RequestParam(value = "before", required = false) String before) {
if (before == null || before.isEmpty()) { if (before == null || before.isEmpty()) {
return extremeValuesService.selectLatestByDeviceId(deviceId); return extremeValuesService.selectLatestByDeviceId(deviceId);
} }
java.util.Date beforeTime = null; Date beforeTime = null;
if (before.matches("\\d+")) { if (before.matches("\\d+")) {
beforeTime = new java.util.Date(Long.parseLong(before)); beforeTime = new Date(Long.parseLong(before));
} else { } else {
try { try {
beforeTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(before); beforeTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(before);

View File

@ -26,8 +26,8 @@ public class VehicleData extends BaseEntity
private String deviceId; private String deviceId;
/** 数据时间戳 */ /** 数据时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date timestamp; private Date timestamp;
/** 车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效) */ /** 车辆状态 (1:启动 2:熄火 3:其他 254:异常 255:无效) */

View File

@ -2,6 +2,7 @@ package com.evobms.project.VehicleData.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
@ -14,105 +15,103 @@ import com.evobms.framework.web.domain.BaseEntity;
* @author ruoyi * @author ruoyi
* @date 2025-11-15 * @date 2025-11-15
*/ */
public class VehicleLocation extends BaseEntity public class VehicleLocation extends BaseEntity {
{
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** 主键ID */ /**
* 主键ID
*/
private Long id; private Long id;
/** 设备ID */ /**
* 设备ID
*/
@Excel(name = "设备ID") @Excel(name = "设备ID")
private String deviceId; private String deviceId;
/** 数据时间戳 */ /**
* 数据时间戳
*/
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "数据时间戳", width = 30, dateFormat = "yyyy-MM-dd")
private Date timestamp; private Date timestamp;
/** 定位状态 (0:无效 1:有效) */ /**
* 定位状态 (0:无效 1:有效)
*/
@Excel(name = "定位状态 (0:无效 1:有效)") @Excel(name = "定位状态 (0:无效 1:有效)")
private Integer positioningStatus; private Integer positioningStatus;
/** 经度 */ /**
* 经度
*/
@Excel(name = "经度") @Excel(name = "经度")
private BigDecimal longitude; private BigDecimal longitude;
/** 纬度 */ /**
* 纬度
*/
@Excel(name = "纬度") @Excel(name = "纬度")
private BigDecimal latitude; private BigDecimal latitude;
public void setId(Long id) public void setId(Long id) {
{
this.id = id; this.id = id;
} }
public Long getId() public Long getId() {
{
return id; return id;
} }
public void setDeviceId(String deviceId) public void setDeviceId(String deviceId) {
{
this.deviceId = deviceId; this.deviceId = deviceId;
} }
public String getDeviceId() public String getDeviceId() {
{
return deviceId; return deviceId;
} }
public void setTimestamp(Date timestamp) public void setTimestamp(Date timestamp) {
{
this.timestamp = timestamp; this.timestamp = timestamp;
} }
public Date getTimestamp() public Date getTimestamp() {
{
return timestamp; return timestamp;
} }
public void setPositioningStatus(Integer positioningStatus) public void setPositioningStatus(Integer positioningStatus) {
{
this.positioningStatus = positioningStatus; this.positioningStatus = positioningStatus;
} }
public Integer getPositioningStatus() public Integer getPositioningStatus() {
{
return positioningStatus; return positioningStatus;
} }
public void setLongitude(BigDecimal longitude) public void setLongitude(BigDecimal longitude) {
{
this.longitude = longitude; this.longitude = longitude;
} }
public BigDecimal getLongitude() public BigDecimal getLongitude() {
{
return longitude; return longitude;
} }
public void setLatitude(BigDecimal latitude) public void setLatitude(BigDecimal latitude) {
{
this.latitude = latitude; this.latitude = latitude;
} }
public BigDecimal getLatitude() public BigDecimal getLatitude() {
{
return latitude; return latitude;
} }
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId()) .append("id", getId())
.append("deviceId", getDeviceId()) .append("deviceId", getDeviceId())
.append("timestamp", getTimestamp()) .append("timestamp", getTimestamp())
.append("positioningStatus", getPositioningStatus()) .append("positioningStatus", getPositioningStatus())
.append("longitude", getLongitude()) .append("longitude", getLongitude())
.append("latitude", getLatitude()) .append("latitude", getLatitude())
.toString(); .toString();
} }
} }

View File

@ -5,17 +5,28 @@ import org.slf4j.LoggerFactory;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
public class GBT32960FullDecoder { public class GBT32960FullDecoder {
private static final Logger log = LoggerFactory.getLogger(GBT32960FullDecoder.class); private static final Logger log = LoggerFactory.getLogger(GBT32960FullDecoder.class);
// 从SN码生成AES密钥 // 从SN码生成AES密钥
private static byte[] generateKeyFromSn(String sn) { private static byte[] generateKeyFromSn(String sn) {
StringBuilder keyStr = new StringBuilder(sn); String s = sn == null ? "" : sn;
while (keyStr.length() < 16) { String key;
keyStr.append("0"); 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();
} }
keyStr = new StringBuilder(keyStr.substring(0, 16)); return key.getBytes(StandardCharsets.UTF_8);
return keyStr.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
} }
// AES解密 // AES解密
@ -173,6 +184,7 @@ public class GBT32960FullDecoder {
//使用的AES密钥 //使用的AES密钥
byte[] aesKey = generateKeyFromSn(deviceSn); byte[] aesKey = generateKeyFromSn(deviceSn);
System.out.println("密钥------------------------>"+aesKey);
byte[] decryptedData = decryptData(encryptedData, aesKey); byte[] decryptedData = decryptData(encryptedData, aesKey);
bytesData = bytesToHex(decryptedData); bytesData = bytesToHex(decryptedData);
log.info("解密成功!"); log.info("解密成功!");

View File

@ -30,6 +30,7 @@ import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.stream.Collectors;
import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.project.system.service.IBmsDevicesService;
import com.evobms.project.system.domain.BmsDevices; import com.evobms.project.system.domain.BmsDevices;
@ -73,7 +74,6 @@ public class MqttService {
private IChargeDischargeSummaryService chargeDischargeSummaryService; private IChargeDischargeSummaryService chargeDischargeSummaryService;
@Value("${mqtt.publishTopic}") @Value("${mqtt.publishTopic}")
private String publishTopic; private String publishTopic;
@ -86,7 +86,7 @@ public class MqttService {
/** /**
* 发送MQTT消息 * 发送MQTT消息
* *
* @param topic 主题 * @param topic 主题
* @param payload 消息内容 * @param payload 消息内容
*/ */
public void sendMessage(String topic, String payload) { public void sendMessage(String topic, String payload) {
@ -100,7 +100,7 @@ public class MqttService {
/** /**
* 发送MQTT字节消息 * 发送MQTT字节消息
* *
* @param topic 主题 * @param topic 主题
* @param payload 原始字节 * @param payload 原始字节
*/ */
public void sendMessageBytes(String topic, byte[] payload) { public void sendMessageBytes(String topic, byte[] payload) {
@ -168,7 +168,7 @@ public class MqttService {
* 发送设备控制命令 * 发送设备控制命令
* *
* @param deviceCode 设备编号 * @param deviceCode 设备编号
* @param command 控制命令 * @param command 控制命令
*/ */
public void sendDeviceCommand(String deviceCode, String command) { public void sendDeviceCommand(String deviceCode, String command) {
Map<String, Object> commandData = new HashMap<>(); Map<String, Object> commandData = new HashMap<>();
@ -185,7 +185,7 @@ public class MqttService {
/** /**
* 处理接收到的设备数据 * 处理接收到的设备数据
* *
* @param topic 主题 * @param topic 主题
* @param message 消息内容 * @param message 消息内容
*/ */
public void handleDeviceData(String topic, byte[] message) { public void handleDeviceData(String topic, byte[] message) {
@ -220,10 +220,11 @@ public class MqttService {
log.error("处理设备数据时发生错误: {}", e.getMessage(), e); log.error("处理设备数据时发生错误: {}", e.getMessage(), e);
} }
} }
/** /**
* 逐字段解析数据字符串 - 直接解析十六进制数据 * 逐字段解析数据字符串 - 直接解析十六进制数据
* *
* @param data 十六进制数据字符串 * @param data 十六进制数据字符串
* @param deviceSn 设备SN码 * @param deviceSn 设备SN码
*/ */
private void parseDataFields(String data, String deviceSn) { private void parseDataFields(String data, String deviceSn) {
@ -252,31 +253,35 @@ public class MqttService {
log.error("设备 {} 解析数据字段时发生错误: {}", deviceSn, e.getMessage(), e); log.error("设备 {} 解析数据字段时发生错误: {}", deviceSn, e.getMessage(), e);
} }
} }
/** /**
* 按照协议定义的索引位置解析各个数据段 * 按照协议定义的索引位置解析各个数据段
*/ */
private void parseByProtocolIndex(byte[] message, String deviceSn) { private void parseByProtocolIndex(byte[] message, String deviceSn) {
try { try {
// 1. 时间信息 - 索引24-29 (6字节) // 1. 时间信息 - 索引24-29 (6字节)
parseTimeAtIndex(message, 0, deviceSn); Date frameTime = parseTimeAtIndex(message, 0, deviceSn);
if (frameTime == null) {
frameTime = new Date();
}
// 2. 整车数据 - 索引30-49 (20字节) // 2. 整车数据 - 索引30-49 (20字节)
parseVehicleDataAtIndex(message, 6, deviceSn); parseVehicleDataAtIndex(message, 6, deviceSn, frameTime);
// 3. 位置信息 - 索引51-60 (10字节) // 3. 位置信息 - 索引51-60 (10字节)
parseLocationDataAtIndex(message, 27, deviceSn); parseLocationDataAtIndex(message, 27, deviceSn, frameTime);
// 4. 极值数据 - 索引61-75 (14字节) // 4. 极值数据 - 索引61-75 (14字节)
parseExtremeDataAtIndex(message, 37, deviceSn); parseExtremeDataAtIndex(message, 37, deviceSn, frameTime);
// 5. 单体电压数据 - 索引76-447 (372字节) // 5. 单体电压数据 - 索引76-447 (372字节)
parseCellVoltageAtIndex(message, 52, deviceSn); parseCellVoltageAtIndex(message, 52, deviceSn, frameTime);
// 6. 单体温度数据 - 索引448-542 (95字节) // 6. 单体温度数据 - 索引448-542 (95字节)
parseCellTemperatureAtIndex(message, 424, deviceSn); parseCellTemperatureAtIndex(message, 424, deviceSn, frameTime);
// 7. 累计充放电量 - 索引543-553 (11字节) // 7. 累计充放电量 - 索引543-553 (11字节)
parseChargeDischargeAtIndex(message, 519, deviceSn); parseChargeDischargeAtIndex(message, 519, deviceSn, frameTime);
log.info("设备 {} 所有数据段解析完成", deviceSn); log.info("设备 {} 所有数据段解析完成", deviceSn);
@ -284,15 +289,16 @@ public class MqttService {
log.error("设备 {} 按索引解析时发生错误: {}", deviceSn, e.getMessage(), e); log.error("设备 {} 按索引解析时发生错误: {}", deviceSn, e.getMessage(), e);
} }
} }
/** /**
* 解析时间信息 - 索引24-29 (6字节) * 解析时间信息 - 索引24-29 (6字节)
* 格式: (偏移2000) + + + + + * 格式: (偏移2000) + + + + +
*/ */
private void parseTimeAtIndex(byte[] message, int startIndex, String deviceSn) { private Date parseTimeAtIndex(byte[] message, int startIndex, String deviceSn) {
try { try {
if (message.length < startIndex + 6) { if (message.length < startIndex + 6) {
log.warn("设备 {}: 时间数据索引越界", deviceSn); log.warn("设备 {}: 时间数据索引越界", deviceSn);
return; return null;
} }
int year = (message[startIndex] & 0xFF) + 2000; // 索引24: int year = (message[startIndex] & 0xFF) + 2000; // 索引24:
@ -307,19 +313,26 @@ public class MqttService {
year, month, day, hour, minute, second); year, month, day, hour, minute, second);
log.info("设备 {} - 时间: {}", deviceSn, time); log.info("设备 {} - 时间: {}", deviceSn, time);
saveTimeData(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 { } else {
log.info("设备 {} - 时间: 无效数据({}-{}-{} {}:{}:{})", log.info("设备 {} - 时间: 无效数据({}-{}-{} {}:{}:{})",
deviceSn, year, month, day, hour, minute, second); deviceSn, year, month, day, hour, minute, second);
return null;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("设备 {} 解析时间数据时发生错误: {}", deviceSn, e.getMessage()); log.error("设备 {} 解析时间数据时发生错误: {}", deviceSn, e.getMessage());
return null;
} }
} }
/** /**
* 解析整车数据 - 索引30-49 (20字节) * 解析整车数据 - 索引30-49 (20字节)
*/ */
private void parseVehicleDataAtIndex(byte[] message, int startIndex, String deviceSn) { private void parseVehicleDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 20) { if (message.length < startIndex + 20) {
log.warn("设备 {}: 整车数据索引越界", deviceSn); log.warn("设备 {}: 整车数据索引越界", deviceSn);
@ -359,7 +372,7 @@ public class MqttService {
log.info("设备 {} - 正极绝缘电阻: {} Ω", deviceSn, insulationResistance); log.info("设备 {} - 正极绝缘电阻: {} Ω", deviceSn, insulationResistance);
// 保存整车数据 // 保存整车数据
saveVehicleData(deviceSn, voltageValue, currentValue, soc, chargeStatus,insulationResistance); saveVehicleData(deviceSn, voltageValue, currentValue, soc, chargeStatus, insulationResistance, frameTime);
} catch (Exception e) { } catch (Exception e) {
log.error("设备 {} 解析整车数据时发生错误: {}", deviceSn, e.getMessage()); log.error("设备 {} 解析整车数据时发生错误: {}", deviceSn, e.getMessage());
@ -370,7 +383,7 @@ public class MqttService {
/** /**
* 解析位置信息 - 索引51-60 (10字节) * 解析位置信息 - 索引51-60 (10字节)
*/ */
private void parseLocationDataAtIndex(byte[] message, int startIndex, String deviceSn) { private void parseLocationDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 10) { if (message.length < startIndex + 10) {
log.warn("设备 {}: 位置数据索引越界", deviceSn); log.warn("设备 {}: 位置数据索引越界", deviceSn);
@ -388,7 +401,8 @@ public class MqttService {
// 索引52: 位置状态 // 索引52: 位置状态
byte locationStatus = message[startIndex + 1]; byte locationStatus = message[startIndex + 1];
// 转成十六进制字符串例如 "00", "01"
String locationStatusHex = String.format("%02X", locationStatus);
// 索引53-56: 经度 (0.000001°) // 索引53-56: 经度 (0.000001°)
long longitude = ((message[startIndex + 2] & 0xFFL) << 24) | long longitude = ((message[startIndex + 2] & 0xFFL) << 24) |
((message[startIndex + 3] & 0xFFL) << 16) | ((message[startIndex + 3] & 0xFFL) << 16) |
@ -404,8 +418,9 @@ public class MqttService {
if (longitude != 0 || latitude != 0) { if (longitude != 0 || latitude != 0) {
double lon = longitude / 1000000.0; double lon = longitude / 1000000.0;
double lat = latitude / 1000000.0; double lat = latitude / 1000000.0;
log.info("设备 {} - 位置: 经度={}°, 纬度={}° (状态:0x{})", deviceSn, lon, lat, String.format("%02X", locationStatus));
processLocationData(deviceSn, lon, lat,String.format("%02X", locationStatus)); log.info("设备 {} - 位置: 经度={}°, 纬度={}° (状态:0x{})", deviceSn, lon, lat, locationStatusHex);
processLocationData(deviceSn, lon, lat, locationStatusHex, frameTime);
} else { } else {
log.info("设备 {} - 位置: 无效数据", deviceSn); log.info("设备 {} - 位置: 无效数据", deviceSn);
} }
@ -418,7 +433,7 @@ public class MqttService {
/** /**
* 解析极值数据 - 索引61-75 (14字节) * 解析极值数据 - 索引61-75 (14字节)
*/ */
private void parseExtremeDataAtIndex(byte[] message, int startIndex, String deviceSn) { private void parseExtremeDataAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 14) { if (message.length < startIndex + 14) {
log.warn("设备 {}: 极值数据索引越界", deviceSn); log.warn("设备 {}: 极值数据索引越界", deviceSn);
@ -468,7 +483,7 @@ public class MqttService {
int minTempValue = minTemperature - 40; int minTempValue = minTemperature - 40;
log.info("设备 {} - 最低温度: 子系统{}-探针{}, {}°C", deviceSn, minTempSystem, minTempProbe, minTempValue); 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); 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) { } catch (Exception e) {
log.error("设备 {} 解析极值数据时发生错误: {}", deviceSn, e.getMessage()); log.error("设备 {} 解析极值数据时发生错误: {}", deviceSn, e.getMessage());
@ -479,7 +494,7 @@ public class MqttService {
/** /**
* 解析单体电压数据 - 索引76-447 (372字节) * 解析单体电压数据 - 索引76-447 (372字节)
*/ */
private void parseCellVoltageAtIndex(byte[] message, int startIndex, String deviceSn) { private void parseCellVoltageAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 372) { if (message.length < startIndex + 372) {
log.warn("设备 {}: 单体电压数据索引越界", deviceSn); log.warn("设备 {}: 单体电压数据索引越界", deviceSn);
@ -535,7 +550,7 @@ public class MqttService {
} }
SubsystemVoltage sv = new SubsystemVoltage(); SubsystemVoltage sv = new SubsystemVoltage();
sv.setDeviceId(deviceSn); sv.setDeviceId(deviceSn);
sv.setTimestamp(new Date()); sv.setTimestamp(frameTime);
sv.setSubsystemCount((int) subsystemCount); sv.setSubsystemCount((int) subsystemCount);
sv.setSubsystemNo((int) subsystemNum); sv.setSubsystemNo((int) subsystemNum);
sv.setSubsystemVoltage(BigDecimal.valueOf(storageVoltageValue)); sv.setSubsystemVoltage(BigDecimal.valueOf(storageVoltageValue));
@ -546,7 +561,7 @@ public class MqttService {
sv.setCreateTime(new Date()); sv.setCreateTime(new Date());
sv.setCreateBy(deviceSn); sv.setCreateBy(deviceSn);
String voltagesStr = voltages.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(",")); String voltagesStr = voltages.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(","));
sv.setBatteryVoltages(voltagesStr); sv.setBatteryVoltages(JSON.toJSONString(voltagesStr));
subsystemVoltageService.insertSubsystemVoltage(sv); subsystemVoltageService.insertSubsystemVoltage(sv);
parseCellVoltagesDetail(message, voltageDataStart, deviceSn, startCellIndex, frameCellCount); parseCellVoltagesDetail(message, voltageDataStart, deviceSn, startCellIndex, frameCellCount);
} else { } else {
@ -562,7 +577,7 @@ public class MqttService {
/** /**
* 解析单体温度数据 - 索引448-542 (95字节) * 解析单体温度数据 - 索引448-542 (95字节)
*/ */
private void parseCellTemperatureAtIndex(byte[] message, int startIndex, String deviceSn) { private void parseCellTemperatureAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 89) { if (message.length < startIndex + 89) {
log.warn("设备 {}: 单体温度数据索引越界", deviceSn); log.warn("设备 {}: 单体温度数据索引越界", deviceSn);
@ -603,14 +618,14 @@ public class MqttService {
} }
SubsystemTemperature st = new SubsystemTemperature(); SubsystemTemperature st = new SubsystemTemperature();
st.setDeviceId(deviceSn); st.setDeviceId(deviceSn);
st.setTimestamp(new Date()); st.setTimestamp(frameTime);
st.setSubsystemCount((int) subsystemCount); st.setSubsystemCount((int) subsystemCount);
st.setSubsystemNo((int) subsystemNum); st.setSubsystemNo((int) subsystemNum);
st.setTempProbeCount(totalTempCount); st.setTempProbeCount(totalTempCount);
st.setCreateTime(new Date()); st.setCreateTime(new Date());
st.setCreateBy(deviceSn); st.setCreateBy(deviceSn);
String tempsStr = temperatures.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(",")); String tempsStr = temperatures.stream().map(Object::toString).collect(Collectors.joining(","));
st.setTemperatureValues(tempsStr); st.setTemperatureValues(JSON.toJSONString(tempsStr));
subsystemTemperatureService.insertSubsystemTemperature(st); subsystemTemperatureService.insertSubsystemTemperature(st);
parseCellTemperaturesDetail(message, tempDataStart, deviceSn, totalTempCount); parseCellTemperaturesDetail(message, tempDataStart, deviceSn, totalTempCount);
} else { } else {
@ -625,7 +640,7 @@ public class MqttService {
/** /**
* 解析累计充放电量 - 索引543-553 (11字节) * 解析累计充放电量 - 索引543-553 (11字节)
*/ */
private void parseChargeDischargeAtIndex(byte[] message, int startIndex, String deviceSn) { private void parseChargeDischargeAtIndex(byte[] message, int startIndex, String deviceSn, Date frameTime) {
try { try {
if (message.length < startIndex + 11) { if (message.length < startIndex + 11) {
log.warn("设备 {}: 充放电数据索引越界", deviceSn); log.warn("设备 {}: 充放电数据索引越界", deviceSn);
@ -659,7 +674,7 @@ public class MqttService {
log.info("设备 {} - 累计充放电量: 放电{}kWh, 充电{}kWh", deviceSn, dischargeKwh, chargeKwh); log.info("设备 {} - 累计充放电量: 放电{}kWh, 充电{}kWh", deviceSn, dischargeKwh, chargeKwh);
saveChargeDischargeSummary(deviceSn, dischargeKwh, chargeKwh); saveChargeDischargeSummary(deviceSn, dischargeKwh, chargeKwh, frameTime);
} catch (Exception e) { } catch (Exception e) {
log.error("设备 {} 解析充放电数据时发生错误: {}", deviceSn, e.getMessage()); log.error("设备 {} 解析充放电数据时发生错误: {}", deviceSn, e.getMessage());
@ -667,8 +682,6 @@ public class MqttService {
} }
/** /**
* 解析详细的单体温度数据 * 解析详细的单体温度数据
*/ */
@ -734,6 +747,7 @@ public class MqttService {
log.info("设备 {} - 电压统计: 最小值{}V, 最大值{}V, 平均值{}V", deviceSn, minVoltage, maxVoltage, avgVoltage); log.info("设备 {} - 电压统计: 最小值{}V, 最大值{}V, 平均值{}V", deviceSn, minVoltage, maxVoltage, avgVoltage);
} }
/** /**
* 检查单体电压数据有效性 * 检查单体电压数据有效性
*/ */
@ -746,18 +760,25 @@ public class MqttService {
} }
return false; return false;
} }
/** /**
* 充电状态描述 * 充电状态描述
*/ */
private int getChargeStatusDescription(byte status) { private int getChargeStatusDescription(byte status) {
/** 充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) */ /** 充电状态 (1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) */
switch (status & 0xFF) { switch (status & 0xFF) {
case 0x01: return 1; case 0x01:
case 0x02: return 2; return 1;
case 0x03: return 3; case 0x02:
case 0x04: return 4; return 2;
case 0xFE: return 254; case 0x03:
case 0xFF: return 255; return 3;
case 0x04:
return 4;
case 0xFE:
return 254;
case 0xFF:
return 255;
} }
return 0; return 0;
@ -774,6 +795,7 @@ public class MqttService {
minute >= 0 && minute <= 59 && minute >= 0 && minute <= 59 &&
second >= 0 && second <= 59; second >= 0 && second <= 59;
} }
/** /**
* 十六进制字符串转字节数组 * 十六进制字符串转字节数组
*/ */
@ -793,8 +815,7 @@ public class MqttService {
*/ */
private void saveCellVoltageData(String deviceSn, byte subsystemNum, int startIndex, int count, List<Double> voltages, double min, double max, double avg) { private void saveCellVoltageData(String deviceSn, byte subsystemNum, int startIndex, int count, List<Double> voltages, double min, double max, double avg) {
// TODO: 实现单体电压数据保存逻辑 // TODO: 实现单体电压数据保存逻辑
log.debug("保存设备 {} 子系统 {} 电压数据: 起始{}, 数量{}, 范围{}-{}V", log.debug("保存设备 {} 子系统 {} 电压数据: 起始{}, 数量{}, 范围{}-{}V", deviceSn, subsystemNum, startIndex, count, min, max);
deviceSn, subsystemNum, startIndex, count, min, max);
} }
/** /**
@ -802,8 +823,7 @@ public class MqttService {
*/ */
private void saveCellTemperatureData(String deviceSn, byte subsystemNum, int count, List<Integer> temperatures, int min, int max, double avg) { private void saveCellTemperatureData(String deviceSn, byte subsystemNum, int count, List<Integer> temperatures, int min, int max, double avg) {
// TODO: 实现单体温度数据保存逻辑 // TODO: 实现单体温度数据保存逻辑
log.debug("保存设备 {} 子系统 {} 温度数据: 数量{}, 范围{}-{}°C", log.debug("保存设备 {} 子系统 {} 温度数据: 数量{}, 范围{}-{}°C", deviceSn, subsystemNum, count, min, max);
deviceSn, subsystemNum, count, min, max);
} }
/** /**
@ -813,13 +833,15 @@ public class MqttService {
// TODO: 实现时间数据保存逻辑 // TODO: 实现时间数据保存逻辑
log.debug("保存设备 {} 时间数据: {}", deviceSn, time); log.debug("保存设备 {} 时间数据: {}", deviceSn, time);
} }
/** /**
* 保存整车数据 * 保存整车数据
*/ */
private void saveVehicleData(String deviceSn, double voltage, double current, int soc, byte chargeStatus,int insulationResistance) { private void saveVehicleData(String deviceSn, double voltage, double current, int soc, byte chargeStatus,int insulationResistance, Date frameTime) {
// 保存整车数据到数据库 // 保存整车数据到数据库
VehicleData ve = new VehicleData(); VehicleData ve = new VehicleData();
ve.setDeviceId(deviceSn); ve.setDeviceId(deviceSn);
ve.setTimestamp(frameTime);
ve.setTotalVoltage(BigDecimal.valueOf(voltage)); ve.setTotalVoltage(BigDecimal.valueOf(voltage));
ve.setTotalCurrent(BigDecimal.valueOf(current)); ve.setTotalCurrent(BigDecimal.valueOf(current));
ve.setSoc(soc); ve.setSoc(soc);
@ -831,11 +853,13 @@ public class MqttService {
} }
private void processLocationData(String deviceSn, double longitude, double latitude,String locationStatus) { private void processLocationData(String deviceSn, double longitude, double latitude,String locationStatus, Date frameTime) {
VehicleLocation ve = new VehicleLocation(); VehicleLocation ve = new VehicleLocation();
ve.setDeviceId(deviceSn); ve.setDeviceId(deviceSn);
ve.setTimestamp(new Date()); ve.setTimestamp(frameTime);
ve.setPositioningStatus(Integer.valueOf(String.format("%02X", locationStatus))); // 十六进制字符串 "00"转成整数 0
int status = Integer.parseInt(locationStatus, 16);
ve.setPositioningStatus(status);
ve.setLongitude(BigDecimal.valueOf(longitude)); ve.setLongitude(BigDecimal.valueOf(longitude));
ve.setLatitude(BigDecimal.valueOf(latitude)); ve.setLatitude(BigDecimal.valueOf(latitude));
ve.setCreateTime(new Date()); ve.setCreateTime(new Date());
@ -844,10 +868,10 @@ public class MqttService {
} }
private void saveExtremeData(String deviceSn, int maxVoltageSubsystemNo, int maxVoltageBatteryNo, double maxVoltageValue, int minVoltageSubsystemNo, int minVoltageBatteryNo, double minVoltageValue, int maxTempSubsystemNo, 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) { int maxTempProbeNo, int maxTempValue, int minTempSubsystemNo, int minTempProbeNo, int minTempValue, Date frameTime) {
ExtremeValues ex = new ExtremeValues(); ExtremeValues ex = new ExtremeValues();
ex.setDeviceId(deviceSn); ex.setDeviceId(deviceSn);
ex.setTimestamp(new Date()); ex.setTimestamp(frameTime);
ex.setMaxVoltageSubsystemNo(maxVoltageSubsystemNo); ex.setMaxVoltageSubsystemNo(maxVoltageSubsystemNo);
ex.setMaxVoltageBatteryNo(maxVoltageBatteryNo); ex.setMaxVoltageBatteryNo(maxVoltageBatteryNo);
ex.setMaxVoltageValue(BigDecimal.valueOf(maxVoltageValue)); ex.setMaxVoltageValue(BigDecimal.valueOf(maxVoltageValue));
@ -863,16 +887,17 @@ public class MqttService {
extremeValuesService.insertExtremeValues(ex); extremeValuesService.insertExtremeValues(ex);
} }
private void saveChargeDischargeSummary(String deviceSn, double dischargeKwh, double chargeKwh) { private void saveChargeDischargeSummary(String deviceSn, double dischargeKwh, double chargeKwh, Date frameTime) {
ChargeDischargeSummary s = new ChargeDischargeSummary(); ChargeDischargeSummary s = new ChargeDischargeSummary();
s.setDeviceId(deviceSn); s.setDeviceId(deviceSn);
s.setTimestamp(new Date()); s.setTimestamp(frameTime);
s.setDischargeKwh(BigDecimal.valueOf(dischargeKwh)); s.setDischargeKwh(BigDecimal.valueOf(dischargeKwh));
s.setChargeKwh(BigDecimal.valueOf(chargeKwh)); s.setChargeKwh(BigDecimal.valueOf(chargeKwh));
s.setCreateTime(new Date()); s.setCreateTime(new Date());
s.setCreateBy(deviceSn); s.setCreateBy(deviceSn);
chargeDischargeSummaryService.insertChargeDischargeSummary(s); chargeDischargeSummaryService.insertChargeDischargeSummary(s);
} }
/** /**
* 处理单体温度数据 * 处理单体温度数据
*/ */
@ -888,70 +913,56 @@ public class MqttService {
} }
/** /**
* 更新设备数据 * 更新设备数据
* *
* @param device 设备对象 * @param device 设备对象
* @param data 数据 * @param data 数据
*/ */
private void updateDeviceData(BmsDevice device, Map<String, Object> data) { private void updateDeviceData(BmsDevice device, Map<String, Object> data) {
if (data.containsKey("voltage")) { if (data.containsKey("voltage")) {
device.setCurrentVoltage(Double.valueOf(data.get("voltage").toString())); device.setCurrentVoltage(Double.valueOf(data.get("voltage").toString()));
} }
if (data.containsKey("current")) { if (data.containsKey("current")) {
device.setCurrentCurrent(Double.valueOf(data.get("current").toString())); device.setCurrentCurrent(Double.valueOf(data.get("current").toString()));
} }
if (data.containsKey("capacity")) { if (data.containsKey("capacity")) {
device.setRemainingCapacity(Double.valueOf(data.get("capacity").toString())); device.setRemainingCapacity(Double.valueOf(data.get("capacity").toString()));
} }
if (data.containsKey("temperature")) { if (data.containsKey("temperature")) {
device.setTemperature(Double.valueOf(data.get("temperature").toString())); device.setTemperature(Double.valueOf(data.get("temperature").toString()));
} }
device.setLastCommTime(new Date()); device.setLastCommTime(new Date());
} }
/** /**
* 发送设备状态查询命令 * 发送设备状态查询命令
* *
* @param deviceCode 设备编号 * @param deviceCode 设备编号
*/ */
public void queryDeviceStatus(String deviceCode) { public void queryDeviceStatus(String deviceCode) {
sendDeviceCommand(deviceCode, "STATUS_QUERY"); sendDeviceCommand(deviceCode, "STATUS_QUERY");
} }
/**
/**
* 根据设备唯一编号(deviceCode)查询系统设备表的SN码 * 根据设备唯一编号(deviceCode)查询系统设备表的SN码
*/ */
private String lookupDeviceSnByCode(String deviceCode) { private String lookupDeviceSnByCode(String deviceCode) {
try { try {
BmsDevices query = new BmsDevices(); BmsDevices query = new BmsDevices();
query.setDeviceId(deviceCode); query.setDeviceId(deviceCode);
List<BmsDevices> list = bmsDevicesService.selectBmsDevicesList(query); List<BmsDevices> list = bmsDevicesService.selectBmsDevicesList(query);
if (list != null && !list.isEmpty()) { if (list != null && !list.isEmpty()) {
String sn = list.get(0).getDeviceSn(); String sn = list.get(0).getDeviceSn();
if (sn != null && !sn.isEmpty()) { if (sn != null && !sn.isEmpty()) {
return sn; return sn;
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("查询设备SN失败: {}", e.getMessage()); log.warn("查询设备SN失败: {}", e.getMessage());
} }
return null; return null;
} }
} }

View File

@ -9,6 +9,10 @@ import com.evobms.project.iot.service.DeviceTokenService;
import com.evobms.project.system.domain.BmsDevices; import com.evobms.project.system.domain.BmsDevices;
import com.evobms.project.system.service.IBmsDevicesService; import com.evobms.project.system.service.IBmsDevicesService;
import com.evobms.project.ota.service.IOtaTasksService; import com.evobms.project.ota.service.IOtaTasksService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.MDC; import org.slf4j.MDC;
@ -25,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
@ -46,6 +51,7 @@ import java.util.Map;
* 路径POST /iot/auth/token * 路径POST /iot/auth/token
*/ */
@RestController @RestController
@Tag(name = "设备鉴权", description = "设备鉴权令牌接口")
@RequestMapping("/iot") @RequestMapping("/iot")
public class IotAuthController { public class IotAuthController {
private static final Logger log = LoggerFactory.getLogger(IotAuthController.class); private static final Logger log = LoggerFactory.getLogger(IotAuthController.class);
@ -89,6 +95,7 @@ public class IotAuthController {
* 返回{ code, success, token } * 返回{ code, success, token }
*/ */
@PostMapping("/auth/token") @PostMapping("/auth/token")
@Operation(summary = "设备请求令牌", description = "通过SN码设备号版本信息生成TOKEN")
public Map<String, Object> requestToken(@RequestBody Map<String, String> body) { public Map<String, Object> requestToken(@RequestBody Map<String, String> body) {
String sn = safeTrim(body.get("sn")); String sn = safeTrim(body.get("sn"));
String model = safeTrim(body.get("device")); String model = safeTrim(body.get("device"));
@ -133,7 +140,7 @@ public class IotAuthController {
return resp; return resp;
} }
// 设备合法改为自定义非JWT令牌长度固定65 // 设备合法改为JWT令牌长度固定65
String token = deviceTokenService.issueTokenForSn(sn); String token = deviceTokenService.issueTokenForSn(sn);
log.info("BBOX设备token: {}", token); log.info("BBOX设备token: {}", token);
log.info("设备令牌签发成功: sn={} model={} version={} tokenLen={}", sn, model, version, token != null ? token.length() : 0); log.info("设备令牌签发成功: sn={} model={} version={} tokenLen={}", sn, model, version, token != null ? token.length() : 0);
@ -162,6 +169,8 @@ public class IotAuthController {
* 请求参数version可选 * 请求参数version可选
* 返回{ code, success, data(Base64) } * 返回{ code, success, data(Base64) }
*/ */
@Operation(summary = " MQTT 登录", description = "获取设备端 MQTT 登录信息")
@ApiOperation(value = "MQTT 登录", notes = "获取设备端 MQTT 登录信息")
@GetMapping("/auth/mqtt") @GetMapping("/auth/mqtt")
public Map<String, Object> getMqttCredentials(@RequestParam(value = "version", required = false) String version, public Map<String, Object> getMqttCredentials(@RequestParam(value = "version", required = false) String version,
HttpServletRequest request) { HttpServletRequest request) {
@ -186,10 +195,11 @@ public class IotAuthController {
if (uri.getPort() != -1) { if (uri.getPort() != -1) {
port = uri.getPort(); port = uri.getPort();
} }
} catch (Exception ignore) {} } catch (Exception ignore) {
}
String use = fixLengthRight(mqttUsername, 8); String use = fixLengthRight(mqttUsername, 8);
String pass = fixLengthRight(mqttPassword,16); String pass = fixLengthRight(mqttPassword, 16);
Map<String, String> data = new HashMap<>(); Map<String, String> data = new HashMap<>();
data.put("clientid", sn); data.put("clientid", sn);
@ -199,10 +209,17 @@ public class IotAuthController {
data.put("passwd", pass); data.put("passwd", pass);
String json = JSON.toJSONString(data); String json = JSON.toJSONString(data);
log.debug("登录mqtt信息====》{}",json); log.debug("登录mqtt信息====》{}", json);
String key16 = fixLengthRight(sn, 16); // AES密钥sn后补0到16位 // AES密钥sn后补0到16位
String encryptedBase64 = AesEcbPkcs5Utils.encryptToAscII16(json, key16); String encryptedBase64;
log.debug("登录mqtt信息加密===================》{}",encryptedBase64); if (sn.equals("EVO0001")) {
String key161 = fixLengthRight("EVO0001", 16);
encryptedBase64 = AesEcbPkcs5Utils.encryptToAscII16(json, key161);
} else {
String keyTail6 = sn.length() >= 16 ? sn.substring(sn.length() - 16) : sn;
encryptedBase64 = AesEcbPkcs5Utils.encryptToAscII16(json, keyTail6);
}
log.debug("登录mqtt信息加密===================》{}", encryptedBase64);
resp.put("code", BboxApiConstants.CODE_SUCCESS); resp.put("code", BboxApiConstants.CODE_SUCCESS);
resp.put("success", "1"); resp.put("success", "1");
resp.put("data", encryptedBase64); resp.put("data", encryptedBase64);
@ -221,6 +238,7 @@ public class IotAuthController {
* 请求参数(body){ "version":"1.0.0" } * 请求参数(body){ "version":"1.0.0" }
* 返回{ code(3位), success(1位), update(1位), version(5位), url(11位), count(3位) } * 返回{ code(3位), success(1位), update(1位), version(5位), url(11位), count(3位) }
*/ */
@ApiOperation(value = "设备登录", notes = "设备登录并上报固件版本服务端判断是否需要OTA升级")
@PostMapping("/auth/login") @PostMapping("/auth/login")
public Map<String, Object> deviceLogin(@RequestBody(required = false) String body, @RequestParam(value = "version", required = false) String versionParam, HttpServletRequest request) { public Map<String, Object> deviceLogin(@RequestBody(required = false) String body, @RequestParam(value = "version", required = false) String versionParam, HttpServletRequest request) {
Map<String, Object> resp = new HashMap<>(); Map<String, Object> resp = new HashMap<>();
@ -285,7 +303,8 @@ public class IotAuthController {
// 目标升级版本直接取设备记录中的固件版本跳过 OTA 任务查询 // 目标升级版本直接取设备记录中的固件版本跳过 OTA 任务查询
String targetVersion = null; String targetVersion = null;
try { targetVersion = device.getFirmwareVersion(); try {
targetVersion = device.getFirmwareVersion();
} catch (Exception ignore) { } catch (Exception ignore) {
} }
@ -362,13 +381,14 @@ public class IotAuthController {
* - 路径`/iot/ota/{num}`num 为三位数字字符串000 起始 * - 路径`/iot/ota/{num}`num 为三位数字字符串000 起始
* - 参数`version`必填目标固件版本号例如 `1.0.1` * - 参数`version`必填目标固件版本号例如 `1.0.1`
* - 响应 * - 响应
* 成功`application/octet-stream` 二进制数据每次返回最多 1024 字节最后一片可能不足 1024 * 成功`application/octet-stream` 二进制数据每次返回最多 1024 字节最后一片可能不足 1024
* 失败`{ code, success }` JSON错误码沿用 `BboxApiConstants` * 失败`{ code, success }` JSON错误码沿用 `BboxApiConstants`
* - 首片摘要计算针对首片1024字节数据生成摘要并作为校验头32字节SHA-256原始摘要 * - 首片摘要计算针对首片1024字节数据生成摘要并作为校验头32字节SHA-256原始摘要
* - 首片结构`[header] + [首片数据(最多1024)]`总长 `headerLen + 1024` * - 首片结构`[header] + [首片数据(最多1024)]`总长 `headerLen + 1024`
* - 后续分片与首片相同每片前置相同模式的校验头摘要基于该分片的数据随后紧跟该分片数据 * - 后续分片与首片相同每片前置相同模式的校验头摘要基于该分片的数据随后紧跟该分片数据
*/ */
@GetMapping("/ota/{num}") @GetMapping("/ota/{num}")
@Operation(summary = "OTA固件分片下载", description = " OTA 固件分片下载接口")
public Object downloadOtaChunk(@PathVariable("num") String num, public Object downloadOtaChunk(@PathVariable("num") String num,
@RequestParam(value = "version", required = false) String version, @RequestParam(value = "version", required = false) String version,
HttpServletRequest request) { HttpServletRequest request) {
@ -453,7 +473,8 @@ public class IotAuthController {
log.info("OTA首片头HEX({}): {}", headerLen, toHex(header)); log.info("OTA首片头HEX({}): {}", headerLen, toHex(header));
int prefixLen = Math.min(64, firstData.length); int prefixLen = Math.min(64, firstData.length);
log.info("OTA首片数据HEX前{}字节: {}", prefixLen, toHex(Arrays.copyOfRange(firstData, 0, prefixLen))); log.info("OTA首片数据HEX前{}字节: {}", prefixLen, toHex(Arrays.copyOfRange(firstData, 0, prefixLen)));
} catch (Exception ignore) {} } catch (Exception ignore) {
}
} else { } else {
// 后续分片从首片实际数据(1024)之后开始切片并在分片前置校验头摘要基于该分片数据 // 后续分片从首片实际数据(1024)之后开始切片并在分片前置校验头摘要基于该分片数据
int firstDataLen = Math.min(otaChunkSize, firmwareBytes); int firstDataLen = Math.min(otaChunkSize, firmwareBytes);
@ -486,8 +507,9 @@ public class IotAuthController {
log.info("OTA分片头HEX({}): {}", headerLen, toHex(header)); log.info("OTA分片头HEX({}): {}", headerLen, toHex(header));
int prefixLen = Math.min(64, dataForDigest.length); int prefixLen = Math.min(64, dataForDigest.length);
log.info("OTA分片数据HEX前{}字节: {}", prefixLen, toHex(Arrays.copyOfRange(dataForDigest, 0, prefixLen))); log.info("OTA分片数据HEX前{}字节: {}", prefixLen, toHex(Arrays.copyOfRange(dataForDigest, 0, prefixLen)));
} catch (Exception ignore) {} } catch (Exception ignore) {
} }
}
log.debug("OTA分片发送: sn={} version={} num={} chunkLen={}", log.debug("OTA分片发送: sn={} version={} num={} chunkLen={}",
sn, version, num, chunk.length); sn, version, num, chunk.length);
@ -497,14 +519,14 @@ public class IotAuthController {
.contentLength(chunk.length) .contentLength(chunk.length)
.body(chunk); .body(chunk);
} catch (Exception e) { } catch (Exception e) {
log.error("OTA分片下载异常: {}", e.getMessage(), e); log.error("OTA分片下载异常: {}", e.getMessage(), e);
err.put("code", BboxApiConstants.CODE_UNAUTHORIZED); err.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
err.put("success", "0"); err.put("success", "0");
return err; return err;
} finally { } finally {
MDC.clear(); MDC.clear();
}
} }
}
/** /**
* 动态计算 OTA 分片总数 * 动态计算 OTA 分片总数
@ -554,7 +576,8 @@ public class IotAuthController {
String rel = base + version + ".bin"; String rel = base + version + ".bin";
Resource res = new ClassPathResource(rel); Resource res = new ClassPathResource(rel);
if (!res.exists()) return null; if (!res.exists()) return null;
try (InputStream in = res.getInputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { try (InputStream in = res.getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buf = new byte[4096]; byte[] buf = new byte[4096];
int r; int r;
while ((r = in.read(buf)) != -1) bos.write(buf, 0, r); while ((r = in.read(buf)) != -1) bos.write(buf, 0, r);
@ -580,7 +603,7 @@ public class IotAuthController {
private static String sha256Hex(byte[] data) throws Exception { private static String sha256Hex(byte[] data) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256"); MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data); byte[] digest = md.digest(data);
StringBuilder sb = new StringBuilder(digest.length ); StringBuilder sb = new StringBuilder(digest.length);
for (byte b : digest) { for (byte b : digest) {
//转为 16 进制字符串 //转为 16 进制字符串
String s = Integer.toHexString(0xff & b); String s = Integer.toHexString(0xff & b);
@ -608,7 +631,9 @@ public class IotAuthController {
private static String fixLengthRight(String s, int len) { private static String fixLengthRight(String s, int len) {
if (s == null) s = ""; if (s == null) s = "";
if (s.length() >= len) return s.substring(0, len); if (s.length() >= len){
return s.substring(0, len);
}
StringBuilder sb = new StringBuilder(len); StringBuilder sb = new StringBuilder(len);
sb.append(s); sb.append(s);
while (sb.length() < len) sb.append('0'); while (sb.length() < len) sb.append('0');

View File

@ -47,7 +47,7 @@ public class BmsDevices extends BaseEntity
@Excel(name = "设备IP地址") @Excel(name = "设备IP地址")
private String ipAddress; private String ipAddress;
/** 固件版本号 */ /** 服务器固件版本号 */
@Excel(name = "固件版本号") @Excel(name = "固件版本号")
private String firmwareVersion; private String firmwareVersion;

View File

@ -6,7 +6,7 @@ ruoyi:
version: 3.9.0 version: 3.9.0
# 版权年份 # 版权年份
copyrightYear: 2025 copyrightYear: 2025
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath # 文件路径
profile: D:/ruoyi/uploadPath profile: D:/ruoyi/uploadPath
# 获取ip地址开关 # 获取ip地址开关
addressEnabled: false addressEnabled: false
@ -15,8 +15,8 @@ ruoyi:
# 开发环境配置 # 开发环境配置
server: server:
# 服务器HTTP端口默认8080 # 服务器HTTP端口支持环境变量覆盖SERVER_PORT默认8080
port: 8080 port: ${SERVER_PORT:8081}
servlet: servlet:
# 应用的访问路径 # 应用的访问路径
context-path: / context-path: /
@ -208,11 +208,11 @@ mqtt:
# 默认QoS等级 # 默认QoS等级
defaultQos: 1 defaultQos: 1
# 订阅主题 # 订阅主题
subscribeTopic: /bbox/EVO0002/# subscribeTopic: "/bbox/+/#"
# 发布主题 # 发布主题
publishTopic: evobms/command publishTopic: evobms/command
# 调试开启HTTP请求打印 # 调试开启HTTP请求打印
debug: debug:
httpdump: httpdump:
enabled: true enabled: true

View File

@ -13,13 +13,13 @@
<result property="tempProbeCount" column="temp_probe_count" /> <result property="tempProbeCount" column="temp_probe_count" />
<result property="temperatureValues" column="temperature_values" /> <result property="temperatureValues" column="temperature_values" />
<result property="createTime" column="create_time" /> <result property="createTime" column="create_time" />
<result property="updatedTime" column="updated_time" /> <result property="updateTime" column="update_time" />
<result property="createBy" column="create_by" /> <result property="createBy" column="create_by" />
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
</resultMap> </resultMap>
<sql id="selectSubsystemTemperatureVo"> <sql id="selectSubsystemTemperatureVo">
select id, device_id, timestamp, subsystem_count, subsystem_no, temp_probe_count, temperature_values, create_time, updated_time, create_by, update_by from subsystem_temperature select id, device_id, timestamp, subsystem_count, subsystem_no, temp_probe_count, temperature_values, create_time, update_time, create_by, update_by from subsystem_temperature
</sql> </sql>
<select id="selectSubsystemTemperatureList" parameterType="SubsystemTemperature" resultMap="SubsystemTemperatureResult"> <select id="selectSubsystemTemperatureList" parameterType="SubsystemTemperature" resultMap="SubsystemTemperatureResult">
@ -31,7 +31,7 @@
<if test="subsystemNo != null "> and subsystem_no = #{subsystemNo}</if> <if test="subsystemNo != null "> and subsystem_no = #{subsystemNo}</if>
<if test="tempProbeCount != null "> and temp_probe_count = #{tempProbeCount}</if> <if test="tempProbeCount != null "> and temp_probe_count = #{tempProbeCount}</if>
<if test="temperatureValues != null and temperatureValues != ''"> and temperature_values = #{temperatureValues}</if> <if test="temperatureValues != null and temperatureValues != ''"> and temperature_values = #{temperatureValues}</if>
<if test="updatedTime != null "> and updated_time = #{updatedTime}</if> <if test="updateTime != null "> and update_time = #{updateTime}</if>
</where> </where>
</select> </select>
@ -50,7 +50,7 @@
<if test="tempProbeCount != null">temp_probe_count,</if> <if test="tempProbeCount != null">temp_probe_count,</if>
<if test="temperatureValues != null">temperature_values,</if> <if test="temperatureValues != null">temperature_values,</if>
<if test="createTime != null">create_time,</if> <if test="createTime != null">create_time,</if>
<if test="updatedTime != null">updated_time,</if> <if test="updateTime != null">update_time,</if>
<if test="createBy != null">create_by,</if> <if test="createBy != null">create_by,</if>
<if test="updateBy != null">update_by,</if> <if test="updateBy != null">update_by,</if>
</trim> </trim>
@ -62,7 +62,7 @@
<if test="tempProbeCount != null">#{tempProbeCount},</if> <if test="tempProbeCount != null">#{tempProbeCount},</if>
<if test="temperatureValues != null">#{temperatureValues},</if> <if test="temperatureValues != null">#{temperatureValues},</if>
<if test="createTime != null">#{createTime},</if> <if test="createTime != null">#{createTime},</if>
<if test="updatedTime != null">#{updatedTime},</if> <if test="updateTime != null">#{updateTime},</if>
<if test="createBy != null">#{createBy},</if> <if test="createBy != null">#{createBy},</if>
<if test="updateBy != null">#{updateBy},</if> <if test="updateBy != null">#{updateBy},</if>
</trim> </trim>
@ -78,7 +78,7 @@
<if test="tempProbeCount != null">temp_probe_count = #{tempProbeCount},</if> <if test="tempProbeCount != null">temp_probe_count = #{tempProbeCount},</if>
<if test="temperatureValues != null">temperature_values = #{temperatureValues},</if> <if test="temperatureValues != null">temperature_values = #{temperatureValues},</if>
<if test="createTime != null">create_time = #{createTime},</if> <if test="createTime != null">create_time = #{createTime},</if>
<if test="updatedTime != null">updated_time = #{updatedTime},</if> <if test="updateTime != null">update_time = #{updateTime},</if>
<if test="createBy != null">create_by = #{createBy},</if> <if test="createBy != null">create_by = #{createBy},</if>
<if test="updateBy != null">update_by = #{updateBy},</if> <if test="updateBy != null">update_by = #{updateBy},</if>
</trim> </trim>