增加了航空器进出港通知接口和无人车状态上报接口
This commit is contained in:
parent
c8dadc2f30
commit
589a212877
@ -128,7 +128,7 @@ curl -G "https://api.example.com/api/v1/vehicles/AV-001/status" \
|
||||
--data-urlencode "fields=vehicleInfo,operationalStatus,controlStatus,motionStatus,safetyStatus,sensorStatus,batteryStatus,communicationStatus"
|
||||
```
|
||||
|
||||
### 4.2 Postman/HTTPie 示例
|
||||
### 4.2 Postman/HTTP 示例
|
||||
HTTPie:
|
||||
```bash
|
||||
http GET https://api.example.com/api/v1/vehicles/AV-001/status \
|
||||
|
||||
@ -230,6 +230,8 @@ data:
|
||||
vehicle-location: /api/VehicleLocationInfo
|
||||
vehicle-state: /api/VehicleStateInfo
|
||||
vehicle-command: /api/VehicleCommandInfo
|
||||
# 通用车辆状态API端点(符合universal_autonomous_vehicle_api规范)
|
||||
universal-status: /api/v1/vehicles/{vehicleId}/status
|
||||
timeout: 1000
|
||||
retry-attempts: 3
|
||||
# 无人车数据持久化配置
|
||||
|
||||
@ -0,0 +1,233 @@
|
||||
package com.qaup.collision.controller;
|
||||
|
||||
import com.qaup.collision.datacollector.model.dto.UniversalApiResponse;
|
||||
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 通用车辆API控制器
|
||||
* 符合universal_autonomous_vehicle_api规范
|
||||
*
|
||||
* 提供标准化的车辆状态查询接口,支持8字段组完整状态数据
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/vehicles")
|
||||
@Tag(name = "通用车辆API", description = "符合universal_autonomous_vehicle_api规范的车辆状态接口")
|
||||
public class UniversalVehicleApiController {
|
||||
|
||||
@Autowired
|
||||
private DataCollectorDao dataCollectorDao;
|
||||
|
||||
/**
|
||||
* 获取车辆状态信息
|
||||
* 符合universal_autonomous_vehicle_api规范的完整8字段组格式
|
||||
*
|
||||
* @param vehicleId 车辆唯一标识
|
||||
* @return 车辆状态数据
|
||||
*/
|
||||
@GetMapping("/{vehicleId}/status")
|
||||
@Operation(
|
||||
summary = "获取车辆状态",
|
||||
description = "获取指定车辆的完整状态信息,包含8个必需字段组:vehicleInfo, operationalStatus, controlStatus, motionStatus, safetyStatus, sensorStatus, batteryStatus, communicationStatus"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "成功获取车辆状态"),
|
||||
@ApiResponse(responseCode = "404", description = "车辆不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "服务器内部错误")
|
||||
})
|
||||
public ResponseEntity<UniversalApiResponse<UniversalVehicleStatusDTO>> getVehicleStatus(
|
||||
@Parameter(description = "车辆唯一标识", required = true, example = "UV001")
|
||||
@PathVariable String vehicleId) {
|
||||
|
||||
try {
|
||||
log.info("接收到车辆状态查询请求: vehicleId={}", vehicleId);
|
||||
|
||||
// 调用DAO层获取车辆状态数据
|
||||
UniversalVehicleStatusDTO statusData = dataCollectorDao.getUniversalVehicleStatus(vehicleId);
|
||||
|
||||
if (statusData != null) {
|
||||
// 构建成功响应
|
||||
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
|
||||
.code(200)
|
||||
.message("成功")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(statusData)
|
||||
.build();
|
||||
|
||||
log.info("成功返回车辆状态: vehicleId={}", vehicleId);
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} else {
|
||||
// 构建车辆不存在响应
|
||||
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
|
||||
.code(404)
|
||||
.message("车辆不存在或状态数据不可用")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(null)
|
||||
.build();
|
||||
|
||||
log.warn("车辆不存在或状态数据不可用: vehicleId={}", vehicleId);
|
||||
return ResponseEntity.status(404).body(response);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取车辆状态异常: vehicleId={}", vehicleId, e);
|
||||
|
||||
// 构建错误响应
|
||||
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
|
||||
.code(500)
|
||||
.message("服务器内部错误: " + e.getMessage())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(null)
|
||||
.error(UniversalApiResponse.ErrorDetails.builder()
|
||||
.type("INTERNAL_SERVER_ERROR")
|
||||
.details(e.getMessage())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.status(500).body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车辆状态信息(简化版本,仅返回核心字段)
|
||||
*
|
||||
* @param vehicleId 车辆唯一标识
|
||||
* @param fields 指定返回的字段组,多个字段用逗号分隔
|
||||
* @return 车辆状态数据
|
||||
*/
|
||||
@GetMapping("/{vehicleId}/status/simple")
|
||||
@Operation(
|
||||
summary = "获取车辆状态(简化版)",
|
||||
description = "获取指定车辆的核心状态信息,可通过fields参数指定返回的字段组"
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "成功获取车辆状态"),
|
||||
@ApiResponse(responseCode = "404", description = "车辆不存在"),
|
||||
@ApiResponse(responseCode = "500", description = "服务器内部错误")
|
||||
})
|
||||
public ResponseEntity<UniversalApiResponse<UniversalVehicleStatusDTO>> getVehicleStatusSimple(
|
||||
@Parameter(description = "车辆唯一标识", required = true, example = "UV001")
|
||||
@PathVariable String vehicleId,
|
||||
@Parameter(description = "指定返回的字段组,多个字段用逗号分隔", example = "vehicleInfo,operationalStatus,batteryStatus")
|
||||
@RequestParam(required = false, defaultValue = "vehicleInfo,operationalStatus,motionStatus,batteryStatus") String fields) {
|
||||
|
||||
try {
|
||||
log.info("接收到车辆状态简化查询请求: vehicleId={}, fields={}", vehicleId, fields);
|
||||
|
||||
// 调用DAO层获取完整车辆状态数据
|
||||
UniversalVehicleStatusDTO fullStatusData = dataCollectorDao.getUniversalVehicleStatus(vehicleId);
|
||||
|
||||
if (fullStatusData != null) {
|
||||
// 根据fields参数过滤字段
|
||||
UniversalVehicleStatusDTO filteredStatusData = filterStatusDataByFields(fullStatusData, fields);
|
||||
|
||||
// 构建成功响应
|
||||
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
|
||||
.code(200)
|
||||
.message("成功")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(filteredStatusData)
|
||||
.build();
|
||||
|
||||
log.info("成功返回车辆简化状态: vehicleId={}, fields={}", vehicleId, fields);
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} else {
|
||||
// 构建车辆不存在响应
|
||||
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
|
||||
.code(404)
|
||||
.message("车辆不存在或状态数据不可用")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(null)
|
||||
.build();
|
||||
|
||||
log.warn("车辆不存在或状态数据不可用: vehicleId={}", vehicleId);
|
||||
return ResponseEntity.status(404).body(response);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取车辆简化状态异常: vehicleId={}, fields={}", vehicleId, fields, e);
|
||||
|
||||
// 构建错误响应
|
||||
UniversalApiResponse<UniversalVehicleStatusDTO> response = UniversalApiResponse.<UniversalVehicleStatusDTO>builder()
|
||||
.code(500)
|
||||
.message("服务器内部错误: " + e.getMessage())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(null)
|
||||
.error(UniversalApiResponse.ErrorDetails.builder()
|
||||
.type("INTERNAL_SERVER_ERROR")
|
||||
.details(e.getMessage())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
return ResponseEntity.status(500).body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定字段过滤状态数据
|
||||
*
|
||||
* @param fullData 完整状态数据
|
||||
* @param fieldsParam 字段参数字符串
|
||||
* @return 过滤后的状态数据
|
||||
*/
|
||||
private UniversalVehicleStatusDTO filterStatusDataByFields(UniversalVehicleStatusDTO fullData, String fieldsParam) {
|
||||
if (fieldsParam == null || fieldsParam.trim().isEmpty()) {
|
||||
return fullData;
|
||||
}
|
||||
|
||||
String[] requestedFields = fieldsParam.toLowerCase().split(",");
|
||||
|
||||
UniversalVehicleStatusDTO.UniversalVehicleStatusDTOBuilder builder = UniversalVehicleStatusDTO.builder();
|
||||
|
||||
for (String field : requestedFields) {
|
||||
field = field.trim();
|
||||
switch (field) {
|
||||
case "vehicleinfo":
|
||||
builder.vehicleInfo(fullData.getVehicleInfo());
|
||||
break;
|
||||
case "operationalstatus":
|
||||
builder.operationalStatus(fullData.getOperationalStatus());
|
||||
break;
|
||||
case "controlstatus":
|
||||
builder.controlStatus(fullData.getControlStatus());
|
||||
break;
|
||||
case "motionstatus":
|
||||
builder.motionStatus(fullData.getMotionStatus());
|
||||
break;
|
||||
case "safetystatus":
|
||||
builder.safetyStatus(fullData.getSafetyStatus());
|
||||
break;
|
||||
case "sensorstatus":
|
||||
builder.sensorStatus(fullData.getSensorStatus());
|
||||
break;
|
||||
case "batterystatus":
|
||||
builder.batteryStatus(fullData.getBatteryStatus());
|
||||
break;
|
||||
case "communicationstatus":
|
||||
builder.communicationStatus(fullData.getCommunicationStatus());
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的字段名: {}", field);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,12 @@ import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
@ -476,4 +481,76 @@ public class DataCollectorDao {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用车辆状态数据 (符合universal_autonomous_vehicle_api规范)
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @return 通用车辆状态数据
|
||||
*/
|
||||
public com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO getUniversalVehicleStatus(String vehicleId) {
|
||||
String url = vehicleBaseUrl + "/api/v1/vehicles/" + vehicleId + "/status";
|
||||
|
||||
try {
|
||||
// 获取认证token
|
||||
String token = authService.getToken();
|
||||
if (token == null) {
|
||||
log.warn("无法获取认证token,跳过车辆 {} 的状态采集", vehicleId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", token); // token已经包含"Bearer "前缀
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
log.debug("调用通用车辆状态API: {}", url);
|
||||
|
||||
// 调用API获取状态数据
|
||||
ResponseEntity<com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO>> response =
|
||||
restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
new ParameterizedTypeReference<com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO>>() {}
|
||||
);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
com.qaup.collision.datacollector.model.dto.UniversalApiResponse<com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO> apiResponse = response.getBody();
|
||||
|
||||
if (apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) {
|
||||
log.debug("成功获取车辆 {} 的通用状态数据", vehicleId);
|
||||
return apiResponse.getData();
|
||||
} else {
|
||||
log.warn("API返回错误: 车辆={}, code={}, message={}",
|
||||
vehicleId, apiResponse.getCode(), apiResponse.getMessage());
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
log.warn("通用车辆状态API调用失败: 车辆={}, status={}", vehicleId, response.getStatusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (HttpClientErrorException e) {
|
||||
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
log.debug("车辆 {} 不存在或不支持通用状态API", vehicleId);
|
||||
} else {
|
||||
log.warn("调用通用车辆状态API出现客户端错误: 车辆={}, status={}, error={}",
|
||||
vehicleId, e.getStatusCode(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
} catch (HttpServerErrorException e) {
|
||||
log.error("调用通用车辆状态API出现服务器错误: 车辆={}, status={}, error={}",
|
||||
vehicleId, e.getStatusCode(), e.getMessage());
|
||||
return null;
|
||||
} catch (ResourceAccessException e) {
|
||||
log.error("调用通用车辆状态API网络连接异常: 车辆={}, error={}", vehicleId, e.getMessage());
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("调用通用车辆状态API发生未知异常: 车辆={}", vehicleId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 电池状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - batteryStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class BatteryStatusDTO {
|
||||
|
||||
/**
|
||||
* 主电池状态
|
||||
*/
|
||||
private MainBatteryDTO mainBattery;
|
||||
|
||||
/**
|
||||
* 主电池状态内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class MainBatteryDTO {
|
||||
/**
|
||||
* 电池电量百分比(0~100)
|
||||
*/
|
||||
private Double chargeLevel;
|
||||
|
||||
/**
|
||||
* 电压(V)
|
||||
*/
|
||||
private Double voltage;
|
||||
|
||||
/**
|
||||
* 电流(A)
|
||||
* 正值=充电,负值=放电
|
||||
*/
|
||||
private Double current;
|
||||
|
||||
/**
|
||||
* 温度(°C)
|
||||
*/
|
||||
private Double temperature;
|
||||
|
||||
/**
|
||||
* 充电状态
|
||||
* 枚举值:CHARGING | DISCHARGING | IDLE | FAULT
|
||||
*/
|
||||
private String chargingStatus;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 通信状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - communicationStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class CommunicationStatusDTO {
|
||||
|
||||
/**
|
||||
* V2X通信状态
|
||||
* 枚举值:CONNECTED | DISCONNECTED | FAULT
|
||||
*/
|
||||
private String v2xStatus;
|
||||
|
||||
/**
|
||||
* 蜂窝信号强度(dBm)
|
||||
*/
|
||||
private Integer cellularSignalStrength;
|
||||
|
||||
/**
|
||||
* WiFi连接状态
|
||||
* 枚举值:CONNECTED | DISCONNECTED | FAULT
|
||||
*/
|
||||
private String wifiStatus;
|
||||
|
||||
/**
|
||||
* 云连接状态
|
||||
* 枚举值:ONLINE | OFFLINE | FAULT
|
||||
*/
|
||||
private String cloudConnectivity;
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 控制状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - controlStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ControlStatusDTO {
|
||||
|
||||
/**
|
||||
* 控制模式
|
||||
* 枚举值:MANUAL | AUTONOMOUS | REMOTE | HYBRID
|
||||
*/
|
||||
private String controlMode;
|
||||
|
||||
/**
|
||||
* 控制权限
|
||||
* 枚举值:DRIVER | SYSTEM | REMOTE_OPERATOR
|
||||
*/
|
||||
private String controlAuthority;
|
||||
|
||||
/**
|
||||
* 是否处于远程控制激活态
|
||||
*/
|
||||
private Boolean remoteControlActive;
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 运动状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - motionStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class MotionStatusDTO {
|
||||
|
||||
/**
|
||||
* 位置信息
|
||||
*/
|
||||
private PositionDTO position;
|
||||
|
||||
/**
|
||||
* 速度信息
|
||||
*/
|
||||
private VelocityDTO velocity;
|
||||
|
||||
/**
|
||||
* 位置信息内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class PositionDTO {
|
||||
/**
|
||||
* 纬度(WGS84 度)
|
||||
*/
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* 经度(WGS84 度)
|
||||
*/
|
||||
private Double longitude;
|
||||
}
|
||||
|
||||
/**
|
||||
* 速度信息内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class VelocityDTO {
|
||||
/**
|
||||
* 速度(m/s)
|
||||
*/
|
||||
private Double speed;
|
||||
|
||||
/**
|
||||
* 方向(radians)
|
||||
*/
|
||||
private Double direction;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 运行状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - operationalStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class OperationalStatusDTO {
|
||||
|
||||
/**
|
||||
* 供电与上电状态
|
||||
* 枚举值:ON | OFF | STANDBY
|
||||
*/
|
||||
private String powerStatus;
|
||||
|
||||
/**
|
||||
* 系统健康概览
|
||||
* 枚举值:HEALTHY | DEGRADED | CRITICAL | FAULT
|
||||
* DEGRADED/CRITICAL 用于运营判定
|
||||
*/
|
||||
private String systemHealth;
|
||||
|
||||
/**
|
||||
* 当前控制模式
|
||||
* 枚举值:MANUAL | ASSISTED | AUTONOMOUS | REMOTE
|
||||
*/
|
||||
private String operationalMode;
|
||||
|
||||
/**
|
||||
* 紧急状态等级
|
||||
* 枚举值:NORMAL | WARNING | EMERGENCY | CRITICAL
|
||||
*/
|
||||
private String emergencyStatus;
|
||||
|
||||
/**
|
||||
* 设备心跳时间(毫秒级 UTC)
|
||||
* 用于在线监控存活判定
|
||||
*/
|
||||
private Long lastHeartbeat;
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 安全状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - safetyStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class SafetyStatusDTO {
|
||||
|
||||
/**
|
||||
* 防撞系统是否激活
|
||||
*/
|
||||
private Boolean collisionAvoidanceActive;
|
||||
|
||||
/**
|
||||
* 紧急制动是否就绪
|
||||
*/
|
||||
private Boolean emergencyBrakingReady;
|
||||
|
||||
/**
|
||||
* 路径规划状态
|
||||
* 枚举值:ACTIVE | INACTIVE | FAULT
|
||||
*/
|
||||
private String pathPlanningStatus;
|
||||
|
||||
/**
|
||||
* 障碍检测状态
|
||||
* 枚举值:ACTIVE | INACTIVE | FAULT
|
||||
*/
|
||||
private String obstacleDetectionStatus;
|
||||
|
||||
/**
|
||||
* MRM(最小风险机动)是否被触发
|
||||
* 仅标识,不含细节
|
||||
*/
|
||||
private Boolean minimumRiskManeuverTriggered;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 传感器状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - sensorStatus字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class SensorStatusDTO {
|
||||
|
||||
/**
|
||||
* GPS传感器状态
|
||||
*/
|
||||
private GpsStatusDTO gps;
|
||||
|
||||
/**
|
||||
* GPS传感器状态内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class GpsStatusDTO {
|
||||
/**
|
||||
* GPS状态
|
||||
* 枚举值:ACTIVE | INACTIVE | FAULT
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* GPS精度(米)
|
||||
*/
|
||||
private Double accuracy;
|
||||
|
||||
/**
|
||||
* 最后更新时间(毫秒级 UTC)
|
||||
*/
|
||||
private Long lastUpdate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 通用API响应格式
|
||||
* 符合universal_autonomous_vehicle_api规范
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class UniversalApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 响应状态码
|
||||
* 200: 成功
|
||||
* 400: 请求参数错误
|
||||
* 401: 未授权
|
||||
* 403: 禁止访问
|
||||
* 404: 资源不存在
|
||||
* 429: 请求过多
|
||||
* 500: 服务器内部错误
|
||||
* 503: 服务不可用
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 响应时间戳(UTC毫秒)
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 错误详情(可选,仅在出错时包含)
|
||||
*/
|
||||
private ErrorDetails error;
|
||||
|
||||
/**
|
||||
* 错误详情内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class ErrorDetails {
|
||||
/**
|
||||
* 错误类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 错误详情描述
|
||||
*/
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* 出错字段(可选)
|
||||
*/
|
||||
private String field;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应
|
||||
*
|
||||
* @param data 响应数据
|
||||
* @param <T> 数据类型
|
||||
* @return 成功响应对象
|
||||
*/
|
||||
public static <T> UniversalApiResponse<T> success(T data) {
|
||||
return UniversalApiResponse.<T>builder()
|
||||
.code(200)
|
||||
.message("success")
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应(带自定义消息)
|
||||
*
|
||||
* @param message 响应消息
|
||||
* @param data 响应数据
|
||||
* @param <T> 数据类型
|
||||
* @return 成功响应对象
|
||||
*/
|
||||
public static <T> UniversalApiResponse<T> success(String message, T data) {
|
||||
return UniversalApiResponse.<T>builder()
|
||||
.code(200)
|
||||
.message(message)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误消息
|
||||
* @param errorType 错误类型
|
||||
* @param errorDetails 错误详情
|
||||
* @param <T> 数据类型
|
||||
* @return 错误响应对象
|
||||
*/
|
||||
public static <T> UniversalApiResponse<T> error(int code, String message, String errorType, String errorDetails) {
|
||||
return UniversalApiResponse.<T>builder()
|
||||
.code(code)
|
||||
.message(message)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.error(ErrorDetails.builder()
|
||||
.type(errorType)
|
||||
.details(errorDetails)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应(带字段信息)
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误消息
|
||||
* @param errorType 错误类型
|
||||
* @param errorDetails 错误详情
|
||||
* @param field 出错字段
|
||||
* @param <T> 数据类型
|
||||
* @return 错误响应对象
|
||||
*/
|
||||
public static <T> UniversalApiResponse<T> error(int code, String message, String errorType, String errorDetails, String field) {
|
||||
return UniversalApiResponse.<T>builder()
|
||||
.code(code)
|
||||
.message(message)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.error(ErrorDetails.builder()
|
||||
.type(errorType)
|
||||
.details(errorDetails)
|
||||
.field(field)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 通用车辆状态DTO
|
||||
* 符合universal_autonomous_vehicle_api规范的完整状态数据结构
|
||||
* 包含所有8个必需字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class UniversalVehicleStatusDTO {
|
||||
|
||||
/**
|
||||
* 车辆信息字段组
|
||||
* 包含:vehicleId
|
||||
*/
|
||||
private VehicleInfoDTO vehicleInfo;
|
||||
|
||||
/**
|
||||
* 运行状态字段组
|
||||
* 包含:powerStatus, systemHealth, operationalMode, emergencyStatus, lastHeartbeat
|
||||
*/
|
||||
private OperationalStatusDTO operationalStatus;
|
||||
|
||||
/**
|
||||
* 控制状态字段组
|
||||
* 包含:controlMode, controlAuthority, remoteControlActive
|
||||
*/
|
||||
private ControlStatusDTO controlStatus;
|
||||
|
||||
/**
|
||||
* 运动状态字段组
|
||||
* 包含:position (latitude, longitude), velocity (speed, direction)
|
||||
*/
|
||||
private MotionStatusDTO motionStatus;
|
||||
|
||||
/**
|
||||
* 安全状态字段组
|
||||
* 包含:collisionAvoidanceActive, emergencyBrakingReady, pathPlanningStatus,
|
||||
* obstacleDetectionStatus, minimumRiskManeuverTriggered
|
||||
*/
|
||||
private SafetyStatusDTO safetyStatus;
|
||||
|
||||
/**
|
||||
* 传感器状态字段组
|
||||
* 包含:gps (status, accuracy, lastUpdate)
|
||||
*/
|
||||
private SensorStatusDTO sensorStatus;
|
||||
|
||||
/**
|
||||
* 电池状态字段组
|
||||
* 包含:mainBattery (chargeLevel, voltage, current, temperature, chargingStatus)
|
||||
*/
|
||||
private BatteryStatusDTO batteryStatus;
|
||||
|
||||
/**
|
||||
* 通信状态字段组
|
||||
* 包含:v2xStatus, cellularSignalStrength, wifiStatus, cloudConnectivity
|
||||
*/
|
||||
private CommunicationStatusDTO communicationStatus;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 车辆信息DTO
|
||||
* 符合universal_autonomous_vehicle_api规范 - vehicleInfo字段组
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class VehicleInfoDTO {
|
||||
|
||||
/**
|
||||
* 车辆唯一标识
|
||||
* 字母数字 3~20 位,示例:AV-001
|
||||
*/
|
||||
private String vehicleId;
|
||||
}
|
||||
@ -656,6 +656,120 @@ public class DataCollectorService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时采集通用车辆状态数据 (符合universal_autonomous_vehicle_api规范)
|
||||
*
|
||||
* 数据来源:无人车厂商数据源配置中的通用状态API端点 (/api/v1/vehicles/{vehicleId}/status)
|
||||
* 说明:采集符合universal_autonomous_vehicle_api规范的完整8字段组状态数据
|
||||
* 重构说明:
|
||||
* - 通用车辆状态数据仅用于实时处理,不存储到数据库
|
||||
* - 数据采集后直接用于WebSocket推送和状态监控
|
||||
* - 不进行数据持久化
|
||||
*/
|
||||
@Scheduled(fixedRateString = "${data.collector.interval}")
|
||||
@Async // 异步执行
|
||||
public void collectUniversalVehicleStatus() {
|
||||
if (collectorDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前缓存中的所有无人车ID列表,用于状态查询
|
||||
List<String> activeUnmannedVehicleIds = activeMovingObjectsCache.values().stream()
|
||||
.filter(obj -> obj.getObjectType() == MovingObjectType.UNMANNED_VEHICLE)
|
||||
.map(MovingObject::getObjectId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
if (activeUnmannedVehicleIds.isEmpty()) {
|
||||
log.debug("当前没有活跃的无人车,跳过通用状态数据采集");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始采集 {} 辆无人车的通用状态数据", activeUnmannedVehicleIds.size());
|
||||
|
||||
// 为每个活跃的无人车采集状态数据
|
||||
for (String vehicleId : activeUnmannedVehicleIds) {
|
||||
try {
|
||||
// 调用DAO层获取通用车辆状态数据
|
||||
com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData =
|
||||
dataCollectorDao.getUniversalVehicleStatus(vehicleId);
|
||||
|
||||
if (statusData != null) {
|
||||
// 数据采集阶段:只采集和缓存状态数据,不进行任何计算
|
||||
// 将状态数据存储到专门的缓存中供后续处理
|
||||
cacheUniversalVehicleStatus(vehicleId, statusData);
|
||||
|
||||
log.debug("成功采集无人车通用状态数据: 车辆ID={}, 电池电量={}%, 运行模式={}, 控制模式={}",
|
||||
vehicleId,
|
||||
statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null
|
||||
? statusData.getBatteryStatus().getMainBattery().getChargeLevel() : "未知",
|
||||
statusData.getOperationalStatus() != null ? statusData.getOperationalStatus().getOperationalMode() : "未知",
|
||||
statusData.getControlStatus() != null ? statusData.getControlStatus().getControlMode() : "未知");
|
||||
|
||||
} else {
|
||||
log.debug("无人车 {} 未返回通用状态数据", vehicleId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集无人车通用状态数据异常: vehicleId={}", vehicleId, e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("通用车辆状态数据采集完成,处理车辆数量: {}", activeUnmannedVehicleIds.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("采集通用车辆状态数据异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存通用车辆状态数据
|
||||
* 将状态数据存储到专门的缓存中供DataProcessingService处理
|
||||
*/
|
||||
private void cacheUniversalVehicleStatus(String vehicleId, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData) {
|
||||
// 使用专门的缓存存储通用状态数据
|
||||
// 注意:这里不影响activeMovingObjectsCache中的位置数据,两者独立存储
|
||||
String cacheKey = "universal_status_" + vehicleId;
|
||||
|
||||
// 创建带时间戳的状态数据包装器
|
||||
UniversalVehicleStatusCacheEntry cacheEntry = UniversalVehicleStatusCacheEntry.builder()
|
||||
.vehicleId(vehicleId)
|
||||
.statusData(statusData)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.dataSource("COLLECTOR")
|
||||
.build();
|
||||
|
||||
// 存储到缓存中(这里简化为使用activeMovingObjectsCache的扩展,实际可以使用Redis等)
|
||||
// 临时解决方案:使用一个Map来存储状态数据
|
||||
getUniversalStatusCache().put(cacheKey, cacheEntry);
|
||||
|
||||
log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}", vehicleId, cacheKey);
|
||||
}
|
||||
|
||||
// 用于存储通用车辆状态数据的缓存
|
||||
private final Map<String, UniversalVehicleStatusCacheEntry> universalStatusCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取通用状态缓存的引用
|
||||
*/
|
||||
public Map<String, UniversalVehicleStatusCacheEntry> getUniversalStatusCache() {
|
||||
return universalStatusCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用车辆状态缓存条目
|
||||
*/
|
||||
@lombok.Data
|
||||
@lombok.Builder
|
||||
@lombok.NoArgsConstructor
|
||||
@lombok.AllArgsConstructor
|
||||
public static class UniversalVehicleStatusCacheEntry {
|
||||
private String vehicleId;
|
||||
private com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData;
|
||||
private Long timestamp;
|
||||
private String dataSource;
|
||||
}
|
||||
|
||||
// 移除周期性处理方法 - 现在由DataProcessingService处理
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent;
|
||||
import com.qaup.collision.websocket.event.TrafficLightStatusEvent;
|
||||
import com.qaup.collision.websocket.event.VehicleCommandEvent;
|
||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||
import com.qaup.collision.websocket.message.CollisionWarningPayload;
|
||||
import com.qaup.collision.websocket.message.MessageTypeConstants;
|
||||
import com.qaup.collision.websocket.message.GeofenceAlertPayload;
|
||||
@ -19,6 +20,7 @@ import com.qaup.collision.websocket.message.RuleStateChangePayload;
|
||||
import com.qaup.collision.websocket.message.RuleViolationPayload;
|
||||
import com.qaup.collision.websocket.message.TrafficLightStatusPayload;
|
||||
import com.qaup.collision.websocket.message.VehicleCommandPayload;
|
||||
import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
|
||||
import com.qaup.collision.websocket.message.UniversalMessage;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import com.qaup.collision.websocket.handler.CollisionWebSocketHandler;
|
||||
@ -27,15 +29,18 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* WebSocket统一消息广播器
|
||||
* 监听所有WebSocket事件,使用原生WebSocket统一推送到控制台前端
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebSocketMessageBroadcaster {
|
||||
|
||||
@ -65,6 +70,38 @@ public class WebSocketMessageBroadcaster {
|
||||
|
||||
broadcastMessageInternal(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理车辆状态更新事件
|
||||
* 符合universal_autonomous_vehicle_api规范
|
||||
*/
|
||||
@EventListener
|
||||
@Async
|
||||
public void handleVehicleStatusUpdate(VehicleStatusUpdateEvent event) {
|
||||
try {
|
||||
if (event.getPayload() == null) {
|
||||
log.warn("车辆状态更新事件负载为空,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
VehicleStatusUpdatePayload payload = event.getPayload();
|
||||
|
||||
UniversalMessage<VehicleStatusUpdatePayload> message = UniversalMessage.<VehicleStatusUpdatePayload>builder()
|
||||
.type(MessageTypeConstants.VEHICLE_STATUS_UPDATE)
|
||||
.payload(payload)
|
||||
.timestamp(event.getTimestamp())
|
||||
.messageId(generateMessageId())
|
||||
.build();
|
||||
|
||||
broadcastMessage(message);
|
||||
|
||||
log.debug("发送车辆状态更新WebSocket消息: vehicleId={}, 消息ID={}",
|
||||
payload.getVehicleId(), message.getMessageId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理车辆状态更新事件异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理车辆指令事件
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
package com.qaup.collision.websocket.event;
|
||||
|
||||
import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload;
|
||||
import lombok.Getter;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 车辆状态更新WebSocket事件
|
||||
* 符合universal_autonomous_vehicle_api规范
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Getter
|
||||
public class VehicleStatusUpdateEvent implements WebSocketEvent {
|
||||
|
||||
private final String eventId;
|
||||
private final long timestamp;
|
||||
private final String eventType;
|
||||
private final VehicleStatusUpdatePayload payload;
|
||||
|
||||
/**
|
||||
* 构造车辆状态更新事件
|
||||
*
|
||||
* @param source 事件源对象
|
||||
* @param payload 车辆状态更新载荷
|
||||
*/
|
||||
public VehicleStatusUpdateEvent(Object source, VehicleStatusUpdatePayload payload) {
|
||||
this.eventId = UUID.randomUUID().toString();
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.eventType = "vehicle_status_update";
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带时间戳的车辆状态更新事件
|
||||
*
|
||||
* @param source 事件源对象
|
||||
* @param payload 车辆状态更新载荷
|
||||
* @param timestamp 事件时间戳
|
||||
*/
|
||||
public VehicleStatusUpdateEvent(Object source, VehicleStatusUpdatePayload payload, long timestamp) {
|
||||
this.eventId = UUID.randomUUID().toString();
|
||||
this.timestamp = timestamp;
|
||||
this.eventType = "vehicle_status_update";
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,13 @@ public final class MessageTypeConstants {
|
||||
* 电子围栏告警消息
|
||||
*/
|
||||
public static final String GEOFENCE_ALERT = "geofence_alert";
|
||||
|
||||
|
||||
/**
|
||||
* 车辆状态更新消息
|
||||
* 符合universal_autonomous_vehicle_api规范
|
||||
*/
|
||||
public static final String VEHICLE_STATUS_UPDATE = "vehicle_status_update";
|
||||
|
||||
// 私有构造函数,防止实例化
|
||||
private MessageTypeConstants() {
|
||||
throw new UnsupportedOperationException("常量类不允许实例化");
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
package com.qaup.collision.websocket.message;
|
||||
|
||||
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
|
||||
/**
|
||||
* 车辆状态更新WebSocket消息载荷
|
||||
* 符合universal_autonomous_vehicle_api规范
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class VehicleStatusUpdatePayload {
|
||||
|
||||
/**
|
||||
* 车辆唯一标识
|
||||
*/
|
||||
private String vehicleId;
|
||||
|
||||
/**
|
||||
* 完整的车辆状态数据
|
||||
* 符合universal_autonomous_vehicle_api规范的8字段组格式
|
||||
*/
|
||||
private UniversalVehicleStatusDTO statusData;
|
||||
|
||||
/**
|
||||
* 状态更新时间戳(毫秒级 UTC)
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 数据源标识
|
||||
* 标识数据来源:COLLECTOR(采集器)、CACHE(缓存)、DATABASE(数据库)
|
||||
*/
|
||||
private String dataSource;
|
||||
|
||||
/**
|
||||
* 更新类型
|
||||
* 标识更新类型:FULL(完整更新)、PARTIAL(部分更新)、HEARTBEAT(心跳更新)
|
||||
*/
|
||||
private String updateType;
|
||||
}
|
||||
@ -0,0 +1,352 @@
|
||||
package com.qaup.collision.datacollector.service;
|
||||
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.datacollector.dao.DataCollectorDao;
|
||||
import com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO;
|
||||
import com.qaup.collision.dataprocessing.service.DataProcessingService;
|
||||
import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.PrecisionModel;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 车辆状态更新完整集成测试
|
||||
* 测试从数据采集→数据处理→WebSocket事件发布的完整流程
|
||||
* 如果任何环节有问题,测试必须失败
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class VehicleStatusUpdateIntegrationTest {
|
||||
|
||||
private DataCollectorService dataCollectorService;
|
||||
private DataProcessingService dataProcessingService;
|
||||
private DataCollectorDao dataCollectorDao;
|
||||
private AuthService authService;
|
||||
private GeometryFactory geometryFactory;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<VehicleStatusUpdateEvent> eventCaptor;
|
||||
|
||||
// 模拟的活跃移动对象缓存
|
||||
private ConcurrentHashMap<String, MovingObject> activeMovingObjectsCache;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 创建几何工厂
|
||||
geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
|
||||
|
||||
// 创建真实的服务实例,连接真实API
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
authService = new AuthService(restTemplate);
|
||||
|
||||
// 设置认证服务配置
|
||||
ReflectionTestUtils.setField(authService, "username", "dianxin");
|
||||
ReflectionTestUtils.setField(authService, "password", "dianxin@123");
|
||||
ReflectionTestUtils.setField(authService, "baseUrl", "http://localhost:8090");
|
||||
ReflectionTestUtils.setField(authService, "loginEndpoint", "/login");
|
||||
ReflectionTestUtils.setField(authService, "refreshEndpoint", "/userInfoController/refreshToken");
|
||||
|
||||
// 创建DAO,使用真实的AuthService
|
||||
dataCollectorDao = new DataCollectorDao(restTemplate, authService);
|
||||
|
||||
// 设置DAO的车辆API配置
|
||||
ReflectionTestUtils.setField(dataCollectorDao, "vehicleBaseUrl", "http://localhost:8090");
|
||||
|
||||
// 创建活跃移动对象缓存
|
||||
activeMovingObjectsCache = new ConcurrentHashMap<>();
|
||||
|
||||
// 创建DataCollectorService实例
|
||||
dataCollectorService = new DataCollectorService();
|
||||
|
||||
// 注入DataCollectorService依赖
|
||||
ReflectionTestUtils.setField(dataCollectorService, "dataCollectorDao", dataCollectorDao);
|
||||
ReflectionTestUtils.setField(dataCollectorService, "eventPublisher", eventPublisher);
|
||||
ReflectionTestUtils.setField(dataCollectorService, "collectorDisabled", false);
|
||||
ReflectionTestUtils.setField(dataCollectorService, "activeMovingObjectsCache", activeMovingObjectsCache);
|
||||
|
||||
// 创建DataProcessingService实例
|
||||
dataProcessingService = new DataProcessingService();
|
||||
|
||||
// 设置ApplicationContext模拟,让DataProcessingService可以获取DataCollectorService
|
||||
when(applicationContext.getBean(DataCollectorService.class)).thenReturn(dataCollectorService);
|
||||
|
||||
// 注入DataProcessingService依赖
|
||||
ReflectionTestUtils.setField(dataProcessingService, "eventPublisher", eventPublisher);
|
||||
ReflectionTestUtils.setField(dataProcessingService, "applicationContext", applicationContext);
|
||||
ReflectionTestUtils.setField(dataProcessingService, "collectorDisabled", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试完整的车辆状态更新处理流程
|
||||
* 1. 模拟活跃无人车缓存数据
|
||||
* 2. 执行状态数据采集
|
||||
* 3. 执行状态数据处理
|
||||
* 4. 验证WebSocket事件发布
|
||||
* 5. 验证事件内容
|
||||
*/
|
||||
@Test
|
||||
void testCompleteVehicleStatusUpdateProcessing() {
|
||||
System.out.println("=== 车辆状态更新完整流程集成测试 ===");
|
||||
|
||||
System.out.println("1. 设置测试数据:添加活跃无人车到缓存...");
|
||||
|
||||
// 添加模拟的无人车到活跃对象缓存
|
||||
String vehicleId1 = "鲁B567";
|
||||
String vehicleId2 = "鲁B579";
|
||||
|
||||
Point position1 = geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879));
|
||||
Point position2 = geometryFactory.createPoint(new Coordinate(120.0844104, 36.35506879));
|
||||
|
||||
MovingObject vehicle1 = MovingObject.builder()
|
||||
.objectId(vehicleId1)
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(vehicleId1)
|
||||
.currentPosition(position1)
|
||||
.currentSpeed(null)
|
||||
.currentHeading(null)
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
MovingObject vehicle2 = MovingObject.builder()
|
||||
.objectId(vehicleId2)
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(vehicleId2)
|
||||
.currentPosition(position2)
|
||||
.currentSpeed(null)
|
||||
.currentHeading(null)
|
||||
.altitude(0.0)
|
||||
.build();
|
||||
|
||||
activeMovingObjectsCache.put(vehicleId1, vehicle1);
|
||||
activeMovingObjectsCache.put(vehicleId2, vehicle2);
|
||||
|
||||
System.out.println("✓ 添加了 " + activeMovingObjectsCache.size() + " 辆无人车到缓存");
|
||||
|
||||
System.out.println("2. 执行车辆状态数据采集...");
|
||||
|
||||
// 执行实际的数据采集方法,这会调用真实API
|
||||
dataCollectorService.collectUniversalVehicleStatus();
|
||||
|
||||
System.out.println("3. 检查状态缓存...");
|
||||
|
||||
// 验证状态数据是否被缓存
|
||||
var statusCache = dataCollectorService.getUniversalStatusCache();
|
||||
System.out.println("✓ 状态缓存中有 " + statusCache.size() + " 个条目");
|
||||
|
||||
System.out.println("4. 执行状态数据处理...");
|
||||
|
||||
// 打印状态缓存的内容
|
||||
for (Map.Entry<String, ?> entry : statusCache.entrySet()) {
|
||||
System.out.println(" 缓存条目: " + entry.getKey() + " -> " + entry.getValue().getClass().getSimpleName());
|
||||
}
|
||||
|
||||
// 执行数据处理,这会发布WebSocket事件
|
||||
dataProcessingService.performPeriodicDataProcessing();
|
||||
|
||||
System.out.println("5. 验证事件发布...");
|
||||
|
||||
try {
|
||||
// 验证事件发布
|
||||
verify(eventPublisher, atLeastOnce()).publishEvent(eventCaptor.capture());
|
||||
|
||||
List<VehicleStatusUpdateEvent> capturedEvents = eventCaptor.getAllValues();
|
||||
assertFalse(capturedEvents.isEmpty(), "如果有活跃车辆和状态数据,必须发布事件");
|
||||
|
||||
System.out.println("✓ 检测到事件发布,共发布了 " + capturedEvents.size() + " 个事件");
|
||||
|
||||
System.out.println("6. 验证事件内容...");
|
||||
|
||||
// 验证每个事件的完整性
|
||||
for (int i = 0; i < capturedEvents.size(); i++) {
|
||||
VehicleStatusUpdateEvent event = capturedEvents.get(i);
|
||||
|
||||
System.out.println(String.format(" 事件[%d]: 车辆ID=%s, 事件类型=%s, 时间戳=%d",
|
||||
i+1, event.getPayload() != null ? event.getPayload().getVehicleId() : "未知",
|
||||
event.getEventType(), event.getTimestamp()));
|
||||
|
||||
// 严格验证必要字段
|
||||
assertNotNull(event.getEventId(), "事件ID不能为空");
|
||||
assertNotNull(event.getEventType(), "事件类型不能为空");
|
||||
assertEquals("vehicle_status_update", event.getEventType(), "事件类型必须是vehicle_status_update");
|
||||
assertNotNull(event.getTimestamp(), "时间戳不能为空");
|
||||
assertNotNull(event.getPayload(), "事件载荷不能为空");
|
||||
|
||||
// 验证载荷内容
|
||||
var payload = event.getPayload();
|
||||
assertNotNull(payload.getVehicleId(), "车辆ID不能为空");
|
||||
assertFalse(payload.getVehicleId().trim().isEmpty(), "车辆ID不能为空字符串");
|
||||
assertNotNull(payload.getUpdateType(), "更新类型不能为空");
|
||||
assertNotNull(payload.getDataSource(), "数据源不能为空");
|
||||
assertNotNull(payload.getStatusData(), "状态数据不能为空");
|
||||
|
||||
// 验证状态数据结构
|
||||
var statusData = payload.getStatusData();
|
||||
assertTrue(statusData.getVehicleInfo() != null ||
|
||||
statusData.getOperationalStatus() != null ||
|
||||
statusData.getControlStatus() != null ||
|
||||
statusData.getMotionStatus() != null ||
|
||||
statusData.getSafetyStatus() != null ||
|
||||
statusData.getSensorStatus() != null ||
|
||||
statusData.getBatteryStatus() != null ||
|
||||
statusData.getCommunicationStatus() != null,
|
||||
"状态数据必须包含至少一个字段组");
|
||||
|
||||
// 验证时间戳合理性
|
||||
long now = System.currentTimeMillis();
|
||||
long eventTime = event.getTimestamp();
|
||||
assertTrue(Math.abs(now - eventTime) < 300000,
|
||||
"事件时间戳应该在5分钟内,实际差值: " + Math.abs(now - eventTime) + "ms");
|
||||
}
|
||||
|
||||
System.out.println("✓ 事件内容验证通过");
|
||||
System.out.println("🎉 车辆状态更新完整流程集成测试成功!");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ 检测到问题:" + e.getMessage());
|
||||
|
||||
// 检查是否没有事件发布
|
||||
try {
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
System.err.println(" 没有事件被发布");
|
||||
System.err.println(" 可能原因:");
|
||||
System.err.println(" 1. Mock服务器未启动(http://localhost:8090)");
|
||||
System.err.println(" 2. 认证失败");
|
||||
System.err.println(" 3. 车辆状态API连接问题");
|
||||
System.err.println(" 4. 状态数据转换失败");
|
||||
System.err.println(" 5. 缓存操作失败");
|
||||
|
||||
fail("车辆状态更新流程失败,应该有事件发布但实际没有");
|
||||
|
||||
} catch (Exception verifyException) {
|
||||
System.err.println(" 事件发布验证失败: " + e.getMessage());
|
||||
fail("车辆状态更新处理流程失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试无活跃车辆情况的处理
|
||||
* 如果没有活跃车辆,应该跳过处理而不是报错
|
||||
*/
|
||||
@Test
|
||||
void testNoActiveVehiclesHandling() {
|
||||
System.out.println("=== 测试无活跃车辆处理 ===");
|
||||
|
||||
// 确保缓存为空
|
||||
activeMovingObjectsCache.clear();
|
||||
|
||||
// 执行数据采集(应该跳过)
|
||||
dataCollectorService.collectUniversalVehicleStatus();
|
||||
|
||||
// 执行数据处理(应该跳过)
|
||||
dataProcessingService.performPeriodicDataProcessing();
|
||||
|
||||
// 验证没有事件发布
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
|
||||
System.out.println("✓ 无活跃车辆处理测试完成(正确跳过处理)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试单独的数据采集功能
|
||||
* 验证数据采集是否能正常工作
|
||||
*/
|
||||
@Test
|
||||
void testDataCollectionOnly() {
|
||||
System.out.println("=== 测试单独数据采集 ===");
|
||||
|
||||
// 添加一辆测试车辆
|
||||
String vehicleId = "鲁B567";
|
||||
Point position = geometryFactory.createPoint(new Coordinate(120.0834104, 36.35406879));
|
||||
|
||||
MovingObject vehicle = MovingObject.builder()
|
||||
.objectId(vehicleId)
|
||||
.objectType(MovingObjectType.UNMANNED_VEHICLE)
|
||||
.objectName(vehicleId)
|
||||
.currentPosition(position)
|
||||
.build();
|
||||
|
||||
activeMovingObjectsCache.put(vehicleId, vehicle);
|
||||
|
||||
// 执行数据采集
|
||||
try {
|
||||
dataCollectorService.collectUniversalVehicleStatus();
|
||||
System.out.println("✓ 数据采集执行完成,未抛出异常");
|
||||
|
||||
// 检查状态缓存
|
||||
var statusCache = dataCollectorService.getUniversalStatusCache();
|
||||
System.out.println("✓ 状态缓存条目数: " + statusCache.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ 数据采集失败: " + e.getMessage());
|
||||
// 这不一定是失败,可能是Mock服务器没启动
|
||||
System.out.println("⚠ 数据采集异常,但这可能是由于Mock服务器未启动");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试配置和依赖注入
|
||||
* 确保所有必要的组件都正确注入
|
||||
*/
|
||||
@Test
|
||||
void testDependencyInjection() {
|
||||
System.out.println("=== 测试依赖注入 ===");
|
||||
|
||||
assertNotNull(dataCollectorService, "DataCollectorService应该被正确注入");
|
||||
assertNotNull(dataProcessingService, "DataProcessingService应该被正确注入");
|
||||
assertNotNull(eventPublisher, "ApplicationEventPublisher应该被正确注入");
|
||||
assertNotNull(activeMovingObjectsCache, "ActiveMovingObjectsCache应该被正确注入");
|
||||
|
||||
System.out.println("✓ 依赖注入验证通过");
|
||||
System.out.println("✓ DataCollectorService实例: " + dataCollectorService.getClass().getSimpleName());
|
||||
System.out.println("✓ DataProcessingService实例: " + dataProcessingService.getClass().getSimpleName());
|
||||
System.out.println("✓ EventPublisher实例: " + eventPublisher.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试缓存操作
|
||||
* 验证状态缓存的读写操作
|
||||
*/
|
||||
@Test
|
||||
void testCacheOperations() {
|
||||
System.out.println("=== 测试缓存操作 ===");
|
||||
|
||||
// 获取状态缓存
|
||||
var statusCache = dataCollectorService.getUniversalStatusCache();
|
||||
assertNotNull(statusCache, "状态缓存不能为空");
|
||||
|
||||
int initialSize = statusCache.size();
|
||||
System.out.println("✓ 初始缓存大小: " + initialSize);
|
||||
|
||||
// 测试缓存清理(如果有数据的话)
|
||||
statusCache.clear();
|
||||
assertEquals(0, statusCache.size(), "缓存清理后应该为空");
|
||||
|
||||
System.out.println("✓ 缓存操作验证通过");
|
||||
}
|
||||
}
|
||||
@ -1340,6 +1340,7 @@ class VehicleState:
|
||||
self.target_lat = None # 目标纬度
|
||||
self.target_lon = None # 目标经度
|
||||
self.is_traffic_light_stop = False # 是否因红灯停车
|
||||
self.remoteControlActive = False # 远程控制是否激活
|
||||
|
||||
def can_be_overridden_by(self, command_type: str) -> bool:
|
||||
"""判断当前指令是否可以被新指令覆盖"""
|
||||
@ -1847,22 +1848,17 @@ last_light_switch_time = time.time()
|
||||
|
||||
def check_auth() -> bool:
|
||||
auth_header = request.headers.get('Authorization')
|
||||
|
||||
|
||||
if not auth_header:
|
||||
print("认证失败: 缺少Authorization头")
|
||||
return False
|
||||
|
||||
# 检查是否包含Bearer前缀
|
||||
if not auth_header.startswith('Bearer '):
|
||||
print("认证失败: 不是Bearer格式")
|
||||
return False
|
||||
|
||||
# 提取token部分
|
||||
token = auth_header[7:] # 去掉 "Bearer " 前缀
|
||||
expected_token = AUTH_TOKEN[7:] # 去掉 "Bearer " 前缀
|
||||
|
||||
result = token == expected_token
|
||||
#print(f"认证结果: {result}")
|
||||
|
||||
# 直接比较完整的Authorization头
|
||||
result = auth_header == AUTH_TOKEN
|
||||
if not result:
|
||||
print(f"认证失败: token不匹配")
|
||||
print(f"收到的token: {auth_header}")
|
||||
print(f"期望的token: {AUTH_TOKEN}")
|
||||
return result
|
||||
|
||||
def to_float_safe(x: object) -> float | None:
|
||||
@ -2166,6 +2162,114 @@ def generate_unmanned_vehicle_location_data() -> list[dict[str, Any]]:
|
||||
|
||||
return unmanned_vehicles
|
||||
|
||||
# 生成符合API规范的完整车辆状态数据
|
||||
def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]:
|
||||
"""生成符合universal_autonomous_vehicle_api规范的完整车辆状态数据"""
|
||||
current_time = time.time()
|
||||
timestamp_ms = int(current_time * 1000)
|
||||
|
||||
# 获取车辆基础数据
|
||||
vehicle_data_item = None
|
||||
for v in unmanned_vehicle_data:
|
||||
if v["vehicleNo"] == vehicle_id:
|
||||
vehicle_data_item = v
|
||||
break
|
||||
|
||||
if not vehicle_data_item:
|
||||
return None
|
||||
|
||||
# 获取车辆状态
|
||||
vehicle_state = vehicle_states.get(vehicle_id)
|
||||
|
||||
# 基础位置和运动数据
|
||||
lat_val = to_float_safe(vehicle_data_item.get("latitude"))
|
||||
lon_val = to_float_safe(vehicle_data_item.get("longitude"))
|
||||
speed_val = to_float_safe(vehicle_data_item.get("speed", 0))
|
||||
direction_val = to_float_safe(vehicle_data_item.get("direction", 0))
|
||||
|
||||
# 生成完整状态数据
|
||||
status_data = {
|
||||
"vehicleInfo": {
|
||||
"vehicleId": vehicle_id
|
||||
},
|
||||
"operationalStatus": {
|
||||
"powerStatus": "ON",
|
||||
"systemHealth": "HEALTHY" if vehicle_state and vehicle_state.is_running else "DEGRADED",
|
||||
"operationalMode": "AUTONOMOUS",
|
||||
"emergencyStatus": "CRITICAL" if vehicle_state and vehicle_state.current_command == "ALERT"
|
||||
else "WARNING" if vehicle_state and vehicle_state.current_command == "WARNING"
|
||||
else "NORMAL",
|
||||
"lastHeartbeat": timestamp_ms
|
||||
},
|
||||
"controlStatus": {
|
||||
"controlMode": "AUTONOMOUS",
|
||||
"controlAuthority": "SYSTEM",
|
||||
"remoteControlActive": vehicle_state.remoteControlActive if vehicle_state else False
|
||||
},
|
||||
"motionStatus": {
|
||||
"position": {
|
||||
"latitude": round(lat_val, 6) if lat_val is not None else 0.0,
|
||||
"longitude": round(lon_val, 6) if lon_val is not None else 0.0
|
||||
},
|
||||
"velocity": {
|
||||
"speed": round(speed_val * 1000 / 3600, 2) if speed_val is not None else 0.0, # 转换为 m/s
|
||||
"direction": round(math.radians(direction_val), 4) if direction_val is not None else 0.0 # 转换为弧度
|
||||
}
|
||||
},
|
||||
"safetyStatus": {
|
||||
"collisionAvoidanceActive": vehicle_state.is_running if vehicle_state else True,
|
||||
"emergencyBrakingReady": True,
|
||||
"pathPlanningStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE",
|
||||
"obstacleDetectionStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE",
|
||||
"minimumRiskManeuverTriggered": vehicle_state.current_command == "ALERT" if vehicle_state else False
|
||||
},
|
||||
"sensorStatus": {
|
||||
"gps": {
|
||||
"status": "ACTIVE",
|
||||
"accuracy": 0.5, # 0.5米精度
|
||||
"lastUpdate": timestamp_ms
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"mainBattery": {
|
||||
"chargeLevel": 85.5, # 85.5%
|
||||
"voltage": 48.2, # 48.2V
|
||||
"current": -15.3, # -15.3A (放电)
|
||||
"temperature": 35.2, # 35.2°C
|
||||
"chargingStatus": "DISCHARGING"
|
||||
}
|
||||
},
|
||||
"communicationStatus": {
|
||||
"v2xStatus": "CONNECTED",
|
||||
"cellularSignalStrength": -65, # -65dBm
|
||||
"wifiStatus": "CONNECTED",
|
||||
"cloudConnectivity": "ONLINE"
|
||||
}
|
||||
}
|
||||
|
||||
return status_data
|
||||
|
||||
def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]:
|
||||
"""根据fields参数过滤车辆状态字段"""
|
||||
if not fields:
|
||||
return status_data
|
||||
|
||||
# 解析fields参数
|
||||
requested_fields = [f.strip() for f in fields.split(',')]
|
||||
filtered_data = {}
|
||||
|
||||
# 可用的字段组
|
||||
available_fields = {
|
||||
"vehicleInfo", "operationalStatus", "controlStatus", "motionStatus",
|
||||
"safetyStatus", "sensorStatus", "batteryStatus", "communicationStatus"
|
||||
}
|
||||
|
||||
for field in requested_fields:
|
||||
if field in available_fields and field in status_data:
|
||||
filtered_data[field] = status_data[field]
|
||||
|
||||
return filtered_data
|
||||
|
||||
# 无人车状态数据生成函数
|
||||
def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]:
|
||||
"""生成无人车状态数据,符合官方API格式"""
|
||||
@ -2512,5 +2616,107 @@ def get_aircraft_status():
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/v1/vehicles/<vehicle_id>/status', methods=['GET', 'OPTIONS'])
|
||||
def get_vehicle_status_universal(vehicle_id):
|
||||
"""通用无人车状态上报接口 - 符合universal_autonomous_vehicle_api规范"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
logging.info(f"🔍 收到车辆状态查询请求: vehicleId={vehicle_id}")
|
||||
|
||||
# JWT认证检查
|
||||
if not check_auth():
|
||||
logging.warning(f"❌ 车辆状态查询认证失败: vehicleId={vehicle_id}")
|
||||
return jsonify({
|
||||
"code": 401,
|
||||
"message": "认证失败",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"data": None
|
||||
}), 401
|
||||
|
||||
# 检查vehicleId格式(字母数字 3~20 位)
|
||||
if not vehicle_id or len(vehicle_id) < 3 or len(vehicle_id) > 20:
|
||||
return jsonify({
|
||||
"code": 400,
|
||||
"message": "Bad Request",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "VALIDATION_ERROR",
|
||||
"details": "vehicleId must be 3-20 alphanumeric characters",
|
||||
"field": "vehicleId"
|
||||
}
|
||||
}), 400
|
||||
|
||||
# 检查车辆是否存在
|
||||
vehicle_exists = False
|
||||
for v in unmanned_vehicle_data:
|
||||
if v["vehicleNo"] == vehicle_id:
|
||||
vehicle_exists = True
|
||||
break
|
||||
|
||||
if not vehicle_exists:
|
||||
return jsonify({
|
||||
"code": 404,
|
||||
"message": "Not Found",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "RESOURCE_NOT_FOUND",
|
||||
"details": f"Vehicle {vehicle_id} not found",
|
||||
"field": "vehicleId"
|
||||
}
|
||||
}), 404
|
||||
|
||||
try:
|
||||
# 更新车辆位置数据
|
||||
current_time = time.time()
|
||||
global last_unmanned_vehicle_update_time
|
||||
elapsed_time = current_time - last_unmanned_vehicle_update_time
|
||||
if elapsed_time >= UPDATE_INTERVAL:
|
||||
for vehicle in unmanned_vehicle_data:
|
||||
update_vehicle_position(vehicle, UPDATE_INTERVAL)
|
||||
vehicle["time"] = int(current_time * 1000)
|
||||
last_unmanned_vehicle_update_time = current_time
|
||||
|
||||
# 生成完整的车辆状态数据
|
||||
status_data = generate_comprehensive_vehicle_status(vehicle_id)
|
||||
if not status_data:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": "Internal Server Error",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "DATA_ERROR",
|
||||
"details": "Failed to generate vehicle status data",
|
||||
"field": None
|
||||
}
|
||||
}), 500
|
||||
|
||||
# 处理fields查询参数
|
||||
fields = request.args.get('fields')
|
||||
if fields:
|
||||
status_data = filter_vehicle_status_fields(status_data, fields)
|
||||
|
||||
# 返回成功响应
|
||||
logging.info(f"🚗 返回车辆状态数据: vehicleId={vehicle_id}, 电量={status_data.get('batteryStatus', {}).get('mainBattery', {}).get('chargeLevel', '未知')}%")
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"data": status_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in get_vehicle_status_universal: {str(e)}")
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": "Internal Server Error",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "SERVER_ERROR",
|
||||
"details": str(e),
|
||||
"field": None
|
||||
}
|
||||
}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='localhost', port=8090, debug=True)
|
||||
@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>冲突检测WebSocket测试工具</title>
|
||||
<title>冲突检测与车辆状态WebSocket测试工具</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
@ -125,6 +125,7 @@
|
||||
<div><strong>车辆指令:</strong> <span id="vehicleCommandCount">0</span></div>
|
||||
<div><strong>规则执行状态:</strong> <span id="ruleExecutionCount">0</span></div>
|
||||
<div><strong>规则状态变更:</strong> <span id="ruleStateChangeCount">0</span></div>
|
||||
<div><strong>车辆状态更新:</strong> <span id="vehicleStatusUpdateCount">0</span></div>
|
||||
<div><strong>心跳响应:</strong> <span id="pongCount">0</span></div>
|
||||
<div><strong>连接确认:</strong> <span id="connectionCount">0</span></div>
|
||||
<div><strong>未知消息:</strong> <span id="unknownCount">0</span></div>
|
||||
@ -134,7 +135,7 @@
|
||||
|
||||
<!-- 冲突检测WebSocket测试 -->
|
||||
<div class="test-section">
|
||||
<h3>🚗 冲突检测WebSocket</h3>
|
||||
<h3>🚗 冲突检测与车辆状态WebSocket</h3>
|
||||
|
||||
<div class="control-group">
|
||||
<label>服务器地址:</label>
|
||||
@ -190,11 +191,15 @@
|
||||
vehicle_command: 0,
|
||||
rule_execution_status: 0,
|
||||
rule_state_change: 0,
|
||||
vehicle_status_update: 0,
|
||||
pong: 0,
|
||||
connection: 0,
|
||||
unknown: 0
|
||||
};
|
||||
|
||||
// 记录未知消息类型以便调试
|
||||
let unknownMessageTypes = new Set();
|
||||
|
||||
// 通用日志函数
|
||||
function log(containerId, message, type = 'info') {
|
||||
const container = document.getElementById(containerId);
|
||||
@ -216,10 +221,27 @@
|
||||
// 更新消息统计
|
||||
function updateMessageStats(messageType) {
|
||||
messageStats.total++;
|
||||
if (messageStats.hasOwnProperty(messageType)) {
|
||||
messageStats[messageType]++;
|
||||
|
||||
// 统一转换为小写以匹配统计键
|
||||
const normalizedType = messageType?.toLowerCase();
|
||||
|
||||
// 处理特殊映射
|
||||
let statsKey = normalizedType;
|
||||
if (normalizedType === 'vehicle_status_update' || normalizedType === 'VEHICLE_STATUS_UPDATE') {
|
||||
statsKey = 'vehicle_status_update';
|
||||
}
|
||||
// 处理aircraftRouteUpdate的映射
|
||||
if (normalizedType === 'aircraftrouteupdate') {
|
||||
statsKey = 'aircraftRouteUpdate';
|
||||
}
|
||||
|
||||
if (messageStats.hasOwnProperty(statsKey)) {
|
||||
messageStats[statsKey]++;
|
||||
} else {
|
||||
messageStats.unknown++;
|
||||
// 调试:记录未知的消息类型
|
||||
unknownMessageTypes.add(messageType);
|
||||
console.log('Unknown message type:', messageType, 'normalized:', normalizedType);
|
||||
}
|
||||
|
||||
// 更新页面显示
|
||||
@ -235,6 +257,7 @@
|
||||
document.getElementById('vehicleCommandCount').textContent = messageStats.vehicle_command;
|
||||
document.getElementById('ruleExecutionCount').textContent = messageStats.rule_execution_status;
|
||||
document.getElementById('ruleStateChangeCount').textContent = messageStats.rule_state_change;
|
||||
document.getElementById('vehicleStatusUpdateCount').textContent = messageStats.vehicle_status_update;
|
||||
document.getElementById('pongCount').textContent = messageStats.pong;
|
||||
document.getElementById('connectionCount').textContent = messageStats.connection;
|
||||
document.getElementById('unknownCount').textContent = messageStats.unknown;
|
||||
@ -318,11 +341,16 @@
|
||||
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
|
||||
const message = JSON.stringify({
|
||||
type: 'subscribe',
|
||||
topics: ['position_update', 'collision_warning', 'rule_violation'],
|
||||
topics: [
|
||||
'position_update',
|
||||
'collision_warning',
|
||||
'rule_violation',
|
||||
'vehicle_status_update'
|
||||
],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
collisionWebSocket.send(message);
|
||||
log('collisionLog', '发送订阅请求', 'info');
|
||||
log('collisionLog', '发送订阅请求(包含车辆状态更新)', 'info');
|
||||
} else {
|
||||
log('collisionLog', 'WebSocket未连接', 'error');
|
||||
}
|
||||
@ -403,11 +431,105 @@
|
||||
const conflictLevel = message.payload?.alertLevel || '未知级别';
|
||||
log('collisionLog', `路径冲突告警: ${object1} vs ${object2} (${conflictType}/${conflictLevel})`, 'error');
|
||||
break;
|
||||
case 'vehicle_status_update':
|
||||
case 'VEHICLE_STATUS_UPDATE': // 可能是大写
|
||||
handleVehicleStatusUpdate(message);
|
||||
break;
|
||||
default:
|
||||
log('collisionLog', `未知消息类型: ${message.type}`, 'info');
|
||||
// 打印完整消息内容以便调试
|
||||
log('collisionLog', `未知消息类型: ${message.type} | 完整消息: ${JSON.stringify(message)}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理车辆状态更新消息
|
||||
function handleVehicleStatusUpdate(message) {
|
||||
const payload = message.payload;
|
||||
if (!payload) {
|
||||
log('collisionLog', '🚗 车辆状态更新: 无载荷数据', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const vehicleId = payload.vehicleId || '未知车辆';
|
||||
const timestamp = payload.timestamp || Date.now();
|
||||
const updateType = payload.updateType || '未知';
|
||||
const dataSource = payload.dataSource || '未知';
|
||||
|
||||
// 解析状态数据
|
||||
const statusData = payload.statusData;
|
||||
let statusInfo = [];
|
||||
|
||||
if (statusData) {
|
||||
// 车辆信息
|
||||
if (statusData.vehicleInfo) {
|
||||
statusInfo.push(`ID:${statusData.vehicleInfo.vehicleId || '未知'}`);
|
||||
}
|
||||
|
||||
// 运行状态
|
||||
if (statusData.operationalStatus) {
|
||||
const ops = statusData.operationalStatus;
|
||||
statusInfo.push(`电源:${ops.powerStatus || '未知'}`);
|
||||
statusInfo.push(`模式:${ops.operationalMode || '未知'}`);
|
||||
statusInfo.push(`健康:${ops.systemHealth || '未知'}`);
|
||||
}
|
||||
|
||||
// 控制状态
|
||||
if (statusData.controlStatus) {
|
||||
const ctrl = statusData.controlStatus;
|
||||
statusInfo.push(`控制:${ctrl.controlMode || '未知'}`);
|
||||
statusInfo.push(`权限:${ctrl.controlAuthority || '未知'}`);
|
||||
if (ctrl.remoteControlActive !== undefined) {
|
||||
statusInfo.push(`远程:${ctrl.remoteControlActive ? '是' : '否'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 运动状态
|
||||
if (statusData.motionStatus) {
|
||||
const motion = statusData.motionStatus;
|
||||
if (motion.position) {
|
||||
statusInfo.push(`位置:(${motion.position.latitude?.toFixed(6)},${motion.position.longitude?.toFixed(6)})`);
|
||||
}
|
||||
if (motion.velocity) {
|
||||
statusInfo.push(`速度:${motion.velocity.speed?.toFixed(2)}km/h`);
|
||||
statusInfo.push(`方向:${motion.velocity.direction?.toFixed(1)}°`);
|
||||
}
|
||||
}
|
||||
|
||||
// 安全状态
|
||||
if (statusData.safetyStatus) {
|
||||
const safety = statusData.safetyStatus;
|
||||
statusInfo.push(`避障:${safety.collisionAvoidanceActive ? '启用' : '禁用'}`);
|
||||
statusInfo.push(`急停:${safety.emergencyBrakingReady ? '就绪' : '未就绪'}`);
|
||||
}
|
||||
|
||||
// 电池状态
|
||||
if (statusData.batteryStatus?.mainBattery) {
|
||||
const battery = statusData.batteryStatus.mainBattery;
|
||||
statusInfo.push(`电量:${battery.chargeLevel}%`);
|
||||
statusInfo.push(`电压:${battery.voltage}V`);
|
||||
statusInfo.push(`充电:${battery.chargingStatus || '未知'}`);
|
||||
}
|
||||
|
||||
// 传感器状态
|
||||
if (statusData.sensorStatus?.gps) {
|
||||
const gps = statusData.sensorStatus.gps;
|
||||
statusInfo.push(`GPS:${gps.status}(精度:${gps.accuracy}m)`);
|
||||
}
|
||||
|
||||
// 通信状态
|
||||
if (statusData.communicationStatus) {
|
||||
const comm = statusData.communicationStatus;
|
||||
statusInfo.push(`V2X:${comm.v2xStatus || '未知'}`);
|
||||
statusInfo.push(`信号:${comm.cellularSignalStrength || '未知'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日志消息
|
||||
const statusStr = statusInfo.length > 0 ? statusInfo.join(' | ') : '无状态数据';
|
||||
const timeStr = new Date(timestamp).toLocaleTimeString();
|
||||
|
||||
log('collisionLog', `🚗 车辆状态更新: ${vehicleId} [${updateType}/${dataSource}] @ ${timeStr} - ${statusStr}`, 'success');
|
||||
}
|
||||
|
||||
// =================== 控制面板 ===================
|
||||
function clearLogs() {
|
||||
document.getElementById('collisionLog').innerHTML = '';
|
||||
@ -452,6 +574,7 @@
|
||||
document.getElementById('vehicleCommandCount').textContent = '0';
|
||||
document.getElementById('ruleExecutionCount').textContent = '0';
|
||||
document.getElementById('ruleStateChangeCount').textContent = '0';
|
||||
document.getElementById('vehicleStatusUpdateCount').textContent = '0';
|
||||
document.getElementById('pongCount').textContent = '0';
|
||||
document.getElementById('connectionCount').textContent = '0';
|
||||
document.getElementById('unknownCount').textContent = '0';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user