Align conflict alerts with runtime collision distance settings
This commit is contained in:
parent
fb649516ee
commit
9342f8a161
@ -16,8 +16,8 @@
|
||||
- 如果前端是浏览器并且跨域访问,后续可能还需要后端补 CORS
|
||||
- 配置修改仅在内存中生效
|
||||
- 服务重启后恢复默认配置
|
||||
- `VehicleRegistry` 是“增量更新”
|
||||
- 本次未传入的历史对象不会被清空
|
||||
- `VehicleRegistry` 是“全量覆盖更新”
|
||||
- 每次请求都会覆盖当前运行时注册表,未传入的历史对象会被清空
|
||||
|
||||
建议前端统一配置接口基础地址,例如:
|
||||
|
||||
@ -35,7 +35,7 @@ POST /api/VehicleRegistry
|
||||
|
||||
### 3.2 用途
|
||||
|
||||
前端将“当前关心的对象及其类型”增量同步给后端。
|
||||
前端将“当前关心的对象及其类型”整体同步给后端。
|
||||
|
||||
该接口会直接影响:
|
||||
|
||||
@ -82,8 +82,8 @@ Content-Type: application/json
|
||||
|
||||
#### `TEQIN`
|
||||
|
||||
- 加入“可控车辆集合”
|
||||
- 加入“受管车辆集合”
|
||||
- 不属于“可控车辆集合”
|
||||
|
||||
#### `HANGKONG`
|
||||
|
||||
@ -107,13 +107,13 @@ Content-Type: application/json
|
||||
"status": "success",
|
||||
"updatedAt": 1742280000000,
|
||||
"updated": 3,
|
||||
"controllableCount": 1,
|
||||
"controllableCount": 2,
|
||||
"typesCount": {
|
||||
"WUREN": 1,
|
||||
"TEQIN": 1,
|
||||
"HANGKONG": 1
|
||||
},
|
||||
"controllableVehicleIDs": ["QN001"]
|
||||
"controllableVehicleIDs": ["QN001", "TQ001"]
|
||||
}
|
||||
```
|
||||
|
||||
@ -124,9 +124,9 @@ Content-Type: application/json
|
||||
| `status` | `string` | 固定为 `success` |
|
||||
| `updatedAt` | `number` | 毫秒时间戳 |
|
||||
| `updated` | `number` | 本次成功处理的条目数 |
|
||||
| `controllableCount` | `number` | 当前系统内全部 `WUREN` 总数 |
|
||||
| `controllableCount` | `number` | 当前系统内全部可控车辆总数,目前包含 `WUREN` 和 `TEQIN` |
|
||||
| `typesCount` | `object` | 本次请求内各类型数量统计 |
|
||||
| `controllableVehicleIDs` | `string[]` | 当前系统内全部可控车辆 ID 列表,即所有 `WUREN` |
|
||||
| `controllableVehicleIDs` | `string[]` | 当前系统内全部可控车辆 ID 列表,目前包含所有 `WUREN` 和 `TEQIN` |
|
||||
|
||||
### 3.10 错误响应示例
|
||||
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package com.qaup.collision.datacollector.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.qaup.collision.datacollector.model.enums.CommandReason;
|
||||
import com.qaup.collision.datacollector.model.enums.CommandType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExternalVehicleCommandRequest {
|
||||
|
||||
private String transId;
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty("vehicleID")
|
||||
private String vehicleId;
|
||||
|
||||
private CommandType commandType;
|
||||
private CommandReason commandReason;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private String signalState;
|
||||
private String intersectionId;
|
||||
private double relativeSpeed;
|
||||
private double relativeMotionX;
|
||||
private double relativeMotionY;
|
||||
private double minDistance;
|
||||
}
|
||||
@ -209,7 +209,11 @@ public class DataCollectorService {
|
||||
|
||||
private void initVehicleManagerWebSocketClient() {
|
||||
if (vehicleManagerProperties == null || !vehicleManagerProperties.isWsEnabled()) {
|
||||
log.info("车辆管理 WebSocket 已禁用(仅使用HTTP拉取无人车数据)");
|
||||
log.info("车辆管理 WebSocket 已禁用(当前仅使用 HTTP 采集无人车状态)");
|
||||
return;
|
||||
}
|
||||
if (!vehicleManagerProperties.isWsConfigurationReady()) {
|
||||
log.warn("车辆管理 WebSocket 配置不完整,已回退到 HTTP 采集无人车状态");
|
||||
return;
|
||||
}
|
||||
if (vehicleManagerWebSocketClient != null) {
|
||||
@ -289,6 +293,9 @@ public class DataCollectorService {
|
||||
}
|
||||
|
||||
private void handleVehicleManagerMessage(JsonNode message) {
|
||||
if (vehicleManagerProperties == null || !vehicleManagerProperties.isWsEnabled() || !vehicleManagerProperties.isWsConfigurationReady()) {
|
||||
return;
|
||||
}
|
||||
if (message == null || message.isNull()) {
|
||||
return;
|
||||
}
|
||||
@ -298,7 +305,6 @@ public class DataCollectorService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String messageName = getText(message, "messageName");
|
||||
JsonNode dataNode = message;
|
||||
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
package com.qaup.collision.pathconflict.event;
|
||||
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.pathconflict.service.VehicleCommandService;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
import com.qaup.collision.websocket.broadcaster.RuleEventWebSocketPublisher;
|
||||
import com.qaup.collision.websocket.message.PathConflictAlertMessage;
|
||||
import com.qaup.collision.pathconflict.model.entity.ObjectRouteAssignment;
|
||||
import com.qaup.collision.pathconflict.repository.ObjectRouteAssignmentRepository;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
@ -17,63 +13,47 @@ import org.springframework.stereotype.Component;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* 冲突告警事件监听器
|
||||
* 负责处理路径冲突告警事件,发送WebSocket消息和车辆控制指令
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ConflictAlertEventListener {
|
||||
|
||||
|
||||
private final RuleEventWebSocketPublisher webSocketPublisher;
|
||||
private final VehicleCommandService vehicleCommandService;
|
||||
private final ObjectRouteAssignmentRepository objectRouteAssignmentRepository;
|
||||
|
||||
/**
|
||||
* 处理冲突告警事件
|
||||
*/
|
||||
|
||||
@Async
|
||||
@EventListener
|
||||
public void handleConflictAlert(ConflictAlertEvent event) {
|
||||
log.info("处理路径冲突告警事件: conflictId={}, alertType={}, alertLevel={}",
|
||||
event.getConflictId().map(String::valueOf).orElse("N/A"),
|
||||
event.getAlertType().map(Enum::name).orElse("N/A"),
|
||||
event.getAlertLevel().map(Enum::name).orElse("N/A"));
|
||||
|
||||
// 只有当告警级别为 WARNING, CRITICAL, EMERGENCY 时才发送给前端和车辆指令
|
||||
if (event.getAlertLevel().isPresent() &&
|
||||
(event.getAlertLevel().get() == ConflictAlertLog.AlertLevel.WARNING ||
|
||||
event.getAlertLevel().get() == ConflictAlertLog.AlertLevel.CRITICAL ||
|
||||
event.getAlertLevel().get() == ConflictAlertLog.AlertLevel.EMERGENCY)) {
|
||||
log.info(
|
||||
"Handling path conflict alert event: conflictId={}, alertType={}, alertLevel={}",
|
||||
event.getConflictId().map(String::valueOf).orElse("N/A"),
|
||||
event.getAlertType().map(Enum::name).orElse("N/A"),
|
||||
event.getAlertLevel().map(Enum::name).orElse("N/A")
|
||||
);
|
||||
|
||||
if (event.getAlertLevel().isPresent()
|
||||
&& (event.getAlertLevel().get() == ConflictAlertLog.AlertLevel.WARNING
|
||||
|| event.getAlertLevel().get() == ConflictAlertLog.AlertLevel.CRITICAL
|
||||
|| event.getAlertLevel().get() == ConflictAlertLog.AlertLevel.EMERGENCY)) {
|
||||
try {
|
||||
// 发送WebSocket消息到控制台
|
||||
sendWebSocketAlert(event);
|
||||
|
||||
// 向涉及的无人车发送控制指令
|
||||
sendVehicleCommands(event);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理冲突告警事件失败: conflictId={}", event.getConflictId().orElse(null), e);
|
||||
log.error("Failed to handle conflict alert event: conflictId={}", event.getConflictId().orElse(null), e);
|
||||
}
|
||||
} else {
|
||||
log.debug("告警级别不符合发送条件,不发送WebSocket告警和车辆指令: conflictId={}, alertLevel={}", event.getConflictId().orElse(null), event.getAlertLevel().orElse(null));
|
||||
log.debug(
|
||||
"Skipping WebSocket alert because alert level is not actionable: conflictId={}, alertLevel={}",
|
||||
event.getConflictId().orElse(null),
|
||||
event.getAlertLevel().orElse(null)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送WebSocket告警消息到控制台
|
||||
*/
|
||||
|
||||
private void sendWebSocketAlert(ConflictAlertEvent event) {
|
||||
try {
|
||||
PathConflictAlertMessage message = PathConflictAlertMessage.builder()
|
||||
.conflictId(event.getConflictId().orElse(null))
|
||||
.alertType(event.getAlertType().orElse(null)) // 直接传递 Optional 里的枚举对象
|
||||
.alertLevel(event.getAlertLevel().orElse(null)) // 直接传递 Optional 里的枚举对象
|
||||
.alertType(event.getAlertType().orElse(null))
|
||||
.alertLevel(event.getAlertLevel().orElse(null))
|
||||
.message(event.getMessage())
|
||||
.object1(PathConflictAlertMessage.ConflictObject.builder()
|
||||
.objectName(event.getObject1Name())
|
||||
@ -83,94 +63,24 @@ public class ConflictAlertEventListener {
|
||||
.objectName(event.getObject2Name())
|
||||
.objectType(event.getObject2Type().name())
|
||||
.build())
|
||||
.position(event.getConflictPointLatitude() != null && event.getConflictPointLongitude() != null ?
|
||||
PathConflictAlertMessage.Position.builder()
|
||||
.position(event.getConflictPointLatitude() != null && event.getConflictPointLongitude() != null
|
||||
? PathConflictAlertMessage.Position.builder()
|
||||
.latitude(event.getConflictPointLatitude())
|
||||
.longitude(event.getConflictPointLongitude())
|
||||
.build() : null)
|
||||
.object1Distance(new BigDecimal(event.getObject1Distance()).setScale(2, RoundingMode.HALF_UP).doubleValue()) // 内联四舍五入
|
||||
.object2Distance(new BigDecimal(event.getObject2Distance()).setScale(2, RoundingMode.HALF_UP).doubleValue()) // 内联四舍五入
|
||||
.timeToConflict1(event.getEstimatedTimeToConflictObj1()) // 新增
|
||||
.timeToConflict2(event.getEstimatedTimeToConflictObj2()) // 新增
|
||||
.timeGap(event.getTimeGapSeconds()) // 新增
|
||||
.build()
|
||||
: null)
|
||||
.object1Distance(new BigDecimal(event.getObject1Distance()).setScale(2, RoundingMode.HALF_UP).doubleValue())
|
||||
.object2Distance(new BigDecimal(event.getObject2Distance()).setScale(2, RoundingMode.HALF_UP).doubleValue())
|
||||
.timeToConflict1(event.getEstimatedTimeToConflictObj1())
|
||||
.timeToConflict2(event.getEstimatedTimeToConflictObj2())
|
||||
.timeGap(event.getTimeGapSeconds())
|
||||
.eventTime(event.getEventTime())
|
||||
.build();
|
||||
|
||||
// 发送WebSocket消息
|
||||
|
||||
webSocketPublisher.publishPathConflictAlert(message);
|
||||
|
||||
log.info("WebSocket告警消息已发送: conflictId={}", event.getConflictId().map(String::valueOf).orElse(null));
|
||||
|
||||
log.info("WebSocket path conflict alert sent: conflictId={}", event.getConflictId().map(String::valueOf).orElse(null));
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket告警消息失败: conflictId={}", event.getConflictId().map(String::valueOf).orElse(null), e);
|
||||
log.error("Failed to send WebSocket path conflict alert: conflictId={}", event.getConflictId().map(String::valueOf).orElse(null), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向涉及的无人车发送控制指令
|
||||
*/
|
||||
private void sendVehicleCommands(ConflictAlertEvent event) {
|
||||
try {
|
||||
// 向对象1发送指令(如果是无人车)
|
||||
objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
event.getObject1Name(), ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.ifPresent(assignment -> sendVehicleCommand(event.getObject1Name(), event));
|
||||
|
||||
// 向对象2发送指令(如果是无人车)
|
||||
objectRouteAssignmentRepository.findFirstByObjectNameAndObjectTypeOrderByAssignedAtDesc(
|
||||
event.getObject2Name(), ObjectRouteAssignment.ObjectType.UNMANNED_VEHICLE)
|
||||
.ifPresent(assignment -> sendVehicleCommand(event.getObject2Name(), event));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送车辆控制指令失败: conflictId={}", event.getConflictId().orElse(null), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定无人车发送控制指令
|
||||
*/
|
||||
private void sendVehicleCommand(String vehicleId, ConflictAlertEvent event) {
|
||||
try {
|
||||
String commandType;
|
||||
String commandMessage;
|
||||
|
||||
// 根据告警级别确定指令类型
|
||||
if (event.isEmergencyAlert()) {
|
||||
commandType = "EMERGENCY_STOP";
|
||||
commandMessage = String.format("紧急告警:检测到路径冲突,立即停车!对象1距离冲突点:%.1f米,对象2距离冲突点:%.1f米,时间差:%.1f秒",
|
||||
event.getObject1Distance(), event.getObject2Distance(), event.getTimeGapSeconds());
|
||||
} else if (event.isAlert()) {
|
||||
commandType = "SLOW_DOWN";
|
||||
commandMessage = String.format("路径冲突告警:前方对象1预计%d秒,对象2预计%d秒到达冲突点,请减速行驶,时间差:%.1f秒",
|
||||
event.getEstimatedTimeToConflictObj1(), event.getEstimatedTimeToConflictObj2(), event.getTimeGapSeconds());
|
||||
} else if (event.isWarning()) {
|
||||
commandType = "CAUTION";
|
||||
commandMessage = String.format("路径冲突预警:前方检测到潜在冲突,请注意观察。对象1距离冲突点:%.1f米,对象2距离冲突点:%.1f米",
|
||||
event.getObject1Distance(), event.getObject2Distance());
|
||||
} else {
|
||||
// 如果是非警告、非关键、非紧急的告警(即旧的INFO级别),则不发送指令。
|
||||
// 在PathConflictDetectionService中已经过滤了INFO级别的事件生成,
|
||||
// 所以理论上此处不会进入此分支,但作为防御性编程,避免意外情况。
|
||||
log.warn("收到非WARNING/CRITICAL/EMERGENCY级别的告警事件,不发送车辆指令: conflictId={}, alertLevel={}",
|
||||
event.getConflictId().orElse(null), event.getAlertLevel().orElse(null));
|
||||
return; // 不发送指令
|
||||
}
|
||||
|
||||
// 发送指令到无人车
|
||||
boolean success = vehicleCommandService.sendCommand(
|
||||
vehicleId, commandType, commandMessage, event.getConflictId().map(String::valueOf).orElse(null));
|
||||
|
||||
if (success) {
|
||||
log.info("车辆控制指令已发送: vehicleId={}, commandType={}, conflictId={}",
|
||||
vehicleId, commandType, event.getConflictId().map(String::valueOf).orElse(null));
|
||||
} else {
|
||||
log.warn("车辆控制指令发送失败: vehicleId={}, conflictId={}",
|
||||
vehicleId, event.getConflictId().map(String::valueOf).orElse(null));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送车辆控制指令时出错: vehicleId={}, conflictId={}",
|
||||
vehicleId, event.getConflictId().map(String::valueOf).orElse(null), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ public class PathConflictDetectionService {
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final CoordinateSystemService coordinateSystemService;
|
||||
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||
private final VehicleCommandService vehicleCommandService;
|
||||
|
||||
private static final int MAX_PREDICTION_TIME_SECONDS = 300;
|
||||
private static final double MIN_TIME_GAP_SECONDS = 30.0;
|
||||
@ -72,6 +73,12 @@ public class PathConflictDetectionService {
|
||||
event.getConflictId(), event.getAlertType(), event.getAlertLevel());
|
||||
}
|
||||
|
||||
try {
|
||||
vehicleCommandService.synchronizeConflictCommands(detectedAlertEvents);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to synchronize vehicle commands for conflict events", e);
|
||||
}
|
||||
|
||||
log.info("Path conflict detection completed, {} events published", detectedAlertEvents.size());
|
||||
}
|
||||
|
||||
|
||||
@ -1,190 +1,358 @@
|
||||
package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.common.model.MovingObject.MovingObjectType;
|
||||
import com.qaup.collision.datacollector.model.dto.ExternalVehicleCommandRequest;
|
||||
import com.qaup.collision.datacollector.model.enums.CommandReason;
|
||||
import com.qaup.collision.datacollector.model.enums.CommandType;
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService.VehicleRegistryType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 车辆指令服务
|
||||
* 负责向无人车发送各种控制指令
|
||||
*
|
||||
* @author AI Assistant
|
||||
* @version 1.0
|
||||
* @since 2025-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleCommandService {
|
||||
|
||||
/**
|
||||
* 向指定车辆发送控制指令
|
||||
*
|
||||
* @param vehicleId 车辆ID
|
||||
* @param commandType 指令类型 (EMERGENCY_STOP, SLOW_DOWN, CAUTION, INFO)
|
||||
* @param message 指令消息
|
||||
* @param conflictId 关联的冲突ID
|
||||
* @return 发送成功返回true
|
||||
*/
|
||||
public boolean sendCommand(String vehicleId, String commandType, String message, String conflictId) {
|
||||
try {
|
||||
log.info("向车辆发送指令: vehicleId={}, commandType={}, message={}, conflictId={}",
|
||||
vehicleId, commandType, message, conflictId);
|
||||
|
||||
// TODO: 实际实现时需要根据车辆通信协议发送指令
|
||||
// 这里可能包括:
|
||||
// 1. HTTP接口调用
|
||||
// 2. MQTT消息发送
|
||||
// 3. WebSocket推送
|
||||
// 4. CAN总线通信
|
||||
// 5. 其他车联网协议
|
||||
|
||||
VehicleCommand command = VehicleCommand.builder()
|
||||
.vehicleId(vehicleId)
|
||||
.commandType(commandType)
|
||||
.message(message)
|
||||
.conflictId(conflictId)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.priority(getCommandPriority(commandType))
|
||||
.build();
|
||||
|
||||
// 模拟发送指令的过程
|
||||
boolean success = sendCommandToVehicle(command);
|
||||
|
||||
if (success) {
|
||||
log.info("车辆指令发送成功: vehicleId={}, commandType={}", vehicleId, commandType);
|
||||
// 可以在这里记录指令发送日志到数据库
|
||||
recordCommandLog(command, true, null);
|
||||
} else {
|
||||
log.warn("车辆指令发送失败: vehicleId={}, commandType={}", vehicleId, commandType);
|
||||
recordCommandLog(command, false, "发送失败");
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
private final Map<String, ActiveVehicleCommandState> activeCommandStates = new ConcurrentHashMap<>();
|
||||
private final AtomicLong transIdSequence = new AtomicLong();
|
||||
|
||||
@Value("${data.collector.vehicle-api.base-url:}")
|
||||
private String vehicleApiBaseUrl;
|
||||
|
||||
@Value("${data.collector.vehicle-api.endpoints.vehicle-command:}")
|
||||
private String vehicleCommandEndpoint;
|
||||
|
||||
public void synchronizeConflictCommands(List<ConflictAlertEvent> activeConflicts) {
|
||||
Map<String, VehicleConflictCommand> currentCommands = new LinkedHashMap<>();
|
||||
for (ConflictAlertEvent event : activeConflicts) {
|
||||
VehicleConflictCommand candidate = toVehicleConflictCommand(event);
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送车辆指令时出错: vehicleId={}, commandType={}", vehicleId, commandType, e);
|
||||
recordCommandLog(VehicleCommand.builder()
|
||||
.vehicleId(vehicleId)
|
||||
|
||||
currentCommands.merge(
|
||||
candidate.vehicleId(),
|
||||
candidate,
|
||||
(left, right) -> left.level().priority() >= right.level().priority() ? left : right
|
||||
);
|
||||
}
|
||||
|
||||
for (VehicleConflictCommand command : currentCommands.values()) {
|
||||
synchronizeVehicleCommand(command);
|
||||
}
|
||||
|
||||
List<String> trackedVehicleIds = new ArrayList<>(activeCommandStates.keySet());
|
||||
for (String vehicleId : trackedVehicleIds) {
|
||||
if (!currentCommands.containsKey(vehicleId)) {
|
||||
sendResumeIfNeeded(vehicleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronizeVehicleCommand(VehicleConflictCommand currentCommand) {
|
||||
ActiveVehicleCommandState previousState = activeCommandStates.get(currentCommand.vehicleId());
|
||||
if (previousState == null) {
|
||||
issueInitialRiskCommand(currentCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
// De-escalation is intentionally handled by a separate recovery flow. WARNING follow-up behavior is not implemented here yet.
|
||||
if (previousState.level() == VehicleRiskLevel.WARNING && currentCommand.level() == VehicleRiskLevel.ALERT) {
|
||||
issueAlertAndParking(currentCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousState.level() == VehicleRiskLevel.ALERT
|
||||
&& !previousState.parkingIssued()
|
||||
&& currentCommand.level() == VehicleRiskLevel.ALERT) {
|
||||
|
||||
boolean parkingIssued = sendCommand(currentCommand, CommandType.PARKING, CommandReason.AIRCRAFT_CROSSING);
|
||||
if (parkingIssued) {
|
||||
activeCommandStates.put(
|
||||
currentCommand.vehicleId(),
|
||||
new ActiveVehicleCommandState(VehicleRiskLevel.ALERT, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void issueInitialRiskCommand(VehicleConflictCommand command) {
|
||||
if (command.level() == VehicleRiskLevel.WARNING) {
|
||||
if (sendCommand(command, CommandType.WARNING, CommandReason.AIRCRAFT_CROSSING)) {
|
||||
activeCommandStates.put(command.vehicleId(), new ActiveVehicleCommandState(VehicleRiskLevel.WARNING, true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
issueAlertAndParking(command);
|
||||
}
|
||||
|
||||
private void issueAlertAndParking(VehicleConflictCommand command) {
|
||||
boolean alertIssued = sendCommand(command, CommandType.ALERT, CommandReason.AIRCRAFT_CROSSING);
|
||||
if (!alertIssued) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean parkingIssued = sendCommand(command, CommandType.PARKING, CommandReason.AIRCRAFT_CROSSING);
|
||||
activeCommandStates.put(command.vehicleId(), new ActiveVehicleCommandState(VehicleRiskLevel.ALERT, parkingIssued));
|
||||
}
|
||||
|
||||
private void sendResumeIfNeeded(String vehicleId) {
|
||||
ActiveVehicleCommandState previousState = activeCommandStates.get(vehicleId);
|
||||
if (previousState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
VehicleConflictCommand resumeContext = new VehicleConflictCommand(
|
||||
vehicleId,
|
||||
previousState.level(),
|
||||
null,
|
||||
null,
|
||||
resolveRegistryType(vehicleId)
|
||||
);
|
||||
|
||||
boolean resumeIssued = sendCommand(resumeContext, CommandType.RESUME, CommandReason.RESUME_TRAFFIC);
|
||||
if (resumeIssued) {
|
||||
activeCommandStates.remove(vehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
private VehicleConflictCommand toVehicleConflictCommand(ConflictAlertEvent event) {
|
||||
if (event == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VehicleRiskLevel level = toRiskLevel(event);
|
||||
if (level == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VehicleConflictCommand leftCandidate = toVehicleConflictCommand(
|
||||
event.getObject1Name(),
|
||||
event.getObject1Type(),
|
||||
level,
|
||||
event.getConflictPoint()
|
||||
);
|
||||
if (leftCandidate != null) {
|
||||
return leftCandidate;
|
||||
}
|
||||
|
||||
return toVehicleConflictCommand(
|
||||
event.getObject2Name(),
|
||||
event.getObject2Type(),
|
||||
level,
|
||||
event.getConflictPoint()
|
||||
);
|
||||
}
|
||||
|
||||
private VehicleConflictCommand toVehicleConflictCommand(
|
||||
String vehicleId,
|
||||
MovingObjectType objectType,
|
||||
VehicleRiskLevel level,
|
||||
Point conflictPoint) {
|
||||
|
||||
if (vehicleId == null || vehicleId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VehicleRegistryType registryType = resolveRegistryType(vehicleId);
|
||||
if (registryType != VehicleRegistryType.WUREN && registryType != VehicleRegistryType.TEQIN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (registryType == VehicleRegistryType.WUREN && objectType != MovingObjectType.UNMANNED_VEHICLE) {
|
||||
return null;
|
||||
}
|
||||
if (registryType == VehicleRegistryType.TEQIN && objectType != MovingObjectType.SPECIAL_VEHICLE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conflictPoint == null) {
|
||||
log.warn("Skipping vehicle command because conflict point is missing: vehicleId={}", vehicleId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VehicleConflictCommand(vehicleId, level, conflictPoint, objectType, registryType);
|
||||
}
|
||||
|
||||
private VehicleRiskLevel toRiskLevel(ConflictAlertEvent event) {
|
||||
if (event.isAlert() || event.isEmergencyAlert()) {
|
||||
return VehicleRiskLevel.ALERT;
|
||||
}
|
||||
if (event.isWarning()) {
|
||||
return VehicleRiskLevel.WARNING;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean sendCommand(
|
||||
VehicleConflictCommand command,
|
||||
CommandType commandType,
|
||||
CommandReason commandReason) {
|
||||
|
||||
if (!isCommandEndpointConfigured()) {
|
||||
log.warn("Skipping vehicle command because vehicle API endpoint is not configured: vehicleId={}", command.vehicleId());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ExternalVehicleCommandRequest request = ExternalVehicleCommandRequest.builder()
|
||||
.transId(nextTransId())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.vehicleId(command.vehicleId())
|
||||
.commandType(commandType)
|
||||
.message(message)
|
||||
.conflictId(conflictId)
|
||||
.build(), false, e.getMessage());
|
||||
.commandReason(commandReason)
|
||||
.latitude(command.conflictPoint() != null ? command.conflictPoint().getY() : 0.0)
|
||||
.longitude(command.conflictPoint() != null ? command.conflictPoint().getX() : 0.0)
|
||||
.signalState("")
|
||||
.intersectionId("")
|
||||
.relativeSpeed(0.0)
|
||||
.relativeMotionX(0.0)
|
||||
.relativeMotionY(0.0)
|
||||
.minDistance(0.0)
|
||||
.build();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
vehicleApiBaseUrl + vehicleCommandEndpoint,
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(request, headers),
|
||||
String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode().value() != 200) {
|
||||
log.warn(
|
||||
"Vehicle command rejected by HTTP status: vehicleId={}, commandType={}, status={}",
|
||||
command.vehicleId(),
|
||||
commandType,
|
||||
response.getStatusCode().value()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonNode responseBody = parseResponseBody(response.getBody());
|
||||
if (responseBody == null || !responseBody.has("code") || !responseBody.has("msg")) {
|
||||
log.warn(
|
||||
"Vehicle command response missing required fields: vehicleId={}, commandType={}, body={}",
|
||||
command.vehicleId(),
|
||||
commandType,
|
||||
response.getBody()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
int responseCode = responseBody.path("code").asInt(Integer.MIN_VALUE);
|
||||
if (responseCode != 200) {
|
||||
log.warn(
|
||||
"Vehicle command rejected by upstream response code: vehicleId={}, commandType={}, code={}, msg={}",
|
||||
command.vehicleId(),
|
||||
commandType,
|
||||
responseCode,
|
||||
responseBody.path("msg").asText()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Vehicle command sent successfully: vehicleId={}, registryType={}, commandType={}, reason={}",
|
||||
command.vehicleId(),
|
||||
command.registryType(),
|
||||
commandType,
|
||||
commandReason
|
||||
);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to send vehicle command: vehicleId={}, commandType={}, reason={}",
|
||||
command.vehicleId(),
|
||||
commandType,
|
||||
commandReason,
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际发送指令到车辆
|
||||
* TODO: 这里需要根据实际的车辆通信方式实现
|
||||
*/
|
||||
private boolean sendCommandToVehicle(VehicleCommand command) {
|
||||
// 根据指令类型选择不同的发送策略
|
||||
switch (command.getCommandType()) {
|
||||
case "EMERGENCY_STOP":
|
||||
return sendEmergencyStopCommand(command);
|
||||
case "SLOW_DOWN":
|
||||
return sendSlowDownCommand(command);
|
||||
case "CAUTION":
|
||||
return sendCautionCommand(command);
|
||||
case "INFO":
|
||||
return sendInfoCommand(command);
|
||||
default:
|
||||
log.warn("未知的指令类型: {}", command.getCommandType());
|
||||
return false;
|
||||
|
||||
private JsonNode parseResponseBody(String body) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readTree(body);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse vehicle command response body: {}", body, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送紧急停车指令
|
||||
*/
|
||||
private boolean sendEmergencyStopCommand(VehicleCommand command) {
|
||||
// TODO: 实现紧急停车指令发送
|
||||
// 这通常需要最高优先级和最快的通信方式
|
||||
log.info("发送紧急停车指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
|
||||
private boolean isCommandEndpointConfigured() {
|
||||
return vehicleApiBaseUrl != null
|
||||
&& !vehicleApiBaseUrl.isBlank()
|
||||
&& vehicleCommandEndpoint != null
|
||||
&& !vehicleCommandEndpoint.isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送减速指令
|
||||
*/
|
||||
private boolean sendSlowDownCommand(VehicleCommand command) {
|
||||
// TODO: 实现减速指令发送
|
||||
log.info("发送减速指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
|
||||
private VehicleRegistryType resolveRegistryType(String vehicleId) {
|
||||
return platformRuntimeStateService.getVehicleRegistryType(vehicleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注意指令
|
||||
*/
|
||||
private boolean sendCautionCommand(VehicleCommand command) {
|
||||
// TODO: 实现注意指令发送
|
||||
log.info("发送注意指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
|
||||
private String nextTransId() {
|
||||
long serialValue = System.currentTimeMillis() * 1000 + Math.floorMod(transIdSequence.incrementAndGet(), 1000);
|
||||
return Long.toHexString(serialValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息指令
|
||||
*/
|
||||
private boolean sendInfoCommand(VehicleCommand command) {
|
||||
// TODO: 实现信息指令发送
|
||||
log.info("发送信息指令: vehicleId={}", command.getVehicleId());
|
||||
|
||||
// 模拟发送成功
|
||||
return true;
|
||||
|
||||
private record VehicleConflictCommand(
|
||||
String vehicleId,
|
||||
VehicleRiskLevel level,
|
||||
Point conflictPoint,
|
||||
MovingObjectType objectType,
|
||||
VehicleRegistryType registryType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指令优先级
|
||||
*/
|
||||
private int getCommandPriority(String commandType) {
|
||||
switch (commandType) {
|
||||
case "EMERGENCY_STOP":
|
||||
return 1; // 最高优先级
|
||||
case "SLOW_DOWN":
|
||||
return 2;
|
||||
case "CAUTION":
|
||||
return 3;
|
||||
case "INFO":
|
||||
return 4; // 最低优先级
|
||||
default:
|
||||
return 5;
|
||||
|
||||
private record ActiveVehicleCommandState(
|
||||
VehicleRiskLevel level,
|
||||
boolean parkingIssued) {
|
||||
}
|
||||
|
||||
private enum VehicleRiskLevel {
|
||||
WARNING(1),
|
||||
ALERT(2);
|
||||
|
||||
private final int priority;
|
||||
|
||||
VehicleRiskLevel(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public int priority() {
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录指令发送日志
|
||||
* TODO: 实际实现时可以保存到数据库
|
||||
*/
|
||||
private void recordCommandLog(VehicleCommand command, boolean success, String errorMessage) {
|
||||
log.debug("记录指令日志: vehicleId={}, commandType={}, success={}, error={}",
|
||||
command.getVehicleId(), command.getCommandType(), success, errorMessage);
|
||||
|
||||
// 这里可以保存到数据库中的指令日志表
|
||||
// 包括发送时间、成功状态、错误信息等
|
||||
}
|
||||
|
||||
/**
|
||||
* 车辆指令内部类
|
||||
*/
|
||||
@lombok.Data
|
||||
@lombok.Builder
|
||||
@lombok.NoArgsConstructor
|
||||
@lombok.AllArgsConstructor
|
||||
public static class VehicleCommand {
|
||||
private String vehicleId;
|
||||
private String commandType;
|
||||
private String message;
|
||||
private String conflictId;
|
||||
private LocalDateTime timestamp;
|
||||
private int priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,8 @@ public class PlatformRuntimeStateService {
|
||||
public VehicleRegistryUpdateResult updateVehicleRegistry(List<VehicleRegistryEntry> entries) {
|
||||
Objects.requireNonNull(entries, "entries");
|
||||
|
||||
vehicleTypes.clear();
|
||||
|
||||
EnumMap<VehicleRegistryType, Integer> requestTypeCounts = new EnumMap<>(VehicleRegistryType.class);
|
||||
for (VehicleRegistryEntry entry : entries) {
|
||||
vehicleTypes.put(entry.vehicleID(), entry.vehicleType());
|
||||
@ -45,7 +47,7 @@ public class PlatformRuntimeStateService {
|
||||
|
||||
Set<String> controllableVehicleIds = new TreeSet<>();
|
||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN || entry.getValue() == VehicleRegistryType.TEQIN) {
|
||||
controllableVehicleIds.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
@ -69,7 +71,17 @@ public class PlatformRuntimeStateService {
|
||||
public List<String> getControllableVehicleIds() {
|
||||
TreeSet<String> vehicleIds = new TreeSet<>();
|
||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN || entry.getValue() == VehicleRegistryType.TEQIN) {
|
||||
vehicleIds.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(new ArrayList<>(vehicleIds));
|
||||
}
|
||||
|
||||
public List<String> getTrafficLightRecipientVehicleIds() {
|
||||
TreeSet<String> vehicleIds = new TreeSet<>();
|
||||
for (Map.Entry<String, VehicleRegistryType> entry : vehicleTypes.entrySet()) {
|
||||
if (entry.getValue() == VehicleRegistryType.WUREN || entry.getValue() == VehicleRegistryType.TEQIN) {
|
||||
vehicleIds.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.qaup.collision.websocket.handler;
|
||||
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.lang.NonNull;
|
||||
@ -27,6 +28,11 @@ public class VehicleCommandInfoWebSocketHandler implements WebSocketHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(VehicleCommandInfoWebSocketHandler.class);
|
||||
|
||||
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
|
||||
private final PlatformRuntimeStateService platformRuntimeStateService;
|
||||
|
||||
public VehicleCommandInfoWebSocketHandler(PlatformRuntimeStateService platformRuntimeStateService) {
|
||||
this.platformRuntimeStateService = platformRuntimeStateService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(@NonNull WebSocketSession session) {
|
||||
@ -67,27 +73,6 @@ public class VehicleCommandInfoWebSocketHandler implements WebSocketHandler {
|
||||
public void sendSignalState(int phaseColor) {
|
||||
String signalState = (phaseColor == 1) ? "GREEN" : "RED";
|
||||
|
||||
String commandJson = String.format(
|
||||
"{" +
|
||||
"\"messageUniqueId\": \"%s\"," +
|
||||
"\"timestamp\": %d," +
|
||||
"\"vehicleID\": \"AET01\"," +
|
||||
"\"commandType\": \"SIGNAL\"," +
|
||||
"\"commandReason\": \"TRAFFIC_LIGHT\"," +
|
||||
"\"signalState\":\"%s\"," +
|
||||
"\"intersectionId\":\"002\"," +
|
||||
"\"latitude\": 343.23," +
|
||||
"\"longitude\": 343.23," +
|
||||
"\"relativeSpeed\": 3," +
|
||||
"\"relativeMotionX\": 2002.12," +
|
||||
"\"relativeMotionY\":100.12," +
|
||||
"\"minDistance\":10.5" +
|
||||
"}",
|
||||
UUID.randomUUID().toString(),
|
||||
System.currentTimeMillis(),
|
||||
signalState
|
||||
);
|
||||
|
||||
LOGGER.info("🚦 [VehicleCommandInfo] 发送车辆控制指令: phaseColor={}, signalState={}", phaseColor, signalState);
|
||||
LOGGER.info("🚦 [VehicleCommandInfo] 当前连接数: {}", sessions.size());
|
||||
|
||||
@ -96,9 +81,45 @@ public class VehicleCommandInfoWebSocketHandler implements WebSocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 向所有连接的客户端发送
|
||||
sessions.values().forEach(session -> sendMessage(session, commandJson));
|
||||
LOGGER.info("🚦 [VehicleCommandInfo] 已向 {} 个客户端发送消息", sessions.size());
|
||||
java.util.List<String> vehicleIds = platformRuntimeStateService.getTrafficLightRecipientVehicleIds();
|
||||
if (vehicleIds.isEmpty()) {
|
||||
LOGGER.info("🚦 [VehicleCommandInfo] 当前无已注册的无人/特勤车辆,跳过发送红绿灯信号");
|
||||
return;
|
||||
}
|
||||
|
||||
int sentMessages = 0;
|
||||
for (String vehicleId : vehicleIds) {
|
||||
String commandJson = buildSignalCommandJson(vehicleId, signalState);
|
||||
for (WebSocketSession session : sessions.values()) {
|
||||
sendMessage(session, commandJson);
|
||||
sentMessages++;
|
||||
}
|
||||
}
|
||||
LOGGER.info("🚦 [VehicleCommandInfo] 已向 {} 个注册车辆发送红绿灯信号,共发送 {} 条消息", vehicleIds.size(), sentMessages);
|
||||
}
|
||||
|
||||
private String buildSignalCommandJson(String vehicleId, String signalState) {
|
||||
return String.format(
|
||||
"{" +
|
||||
"\"messageUniqueId\": \"%s\"," +
|
||||
"\"timestamp\": %d," +
|
||||
"\"vehicleID\": \"%s\"," +
|
||||
"\"commandType\": \"SIGNAL\"," +
|
||||
"\"commandReason\": \"TRAFFIC_LIGHT\"," +
|
||||
"\"signalState\":\"%s\"," +
|
||||
"\"intersectionId\":\"002\"," +
|
||||
"\"latitude\": 343.23," +
|
||||
"\"longitude\": 343.23," +
|
||||
"\"relativeSpeed\": 3," +
|
||||
"\"relativeMotionX\": 2002.12," +
|
||||
"\"relativeMotionY\":100.12," +
|
||||
"\"minDistance\":10.5" +
|
||||
"}",
|
||||
UUID.randomUUID(),
|
||||
System.currentTimeMillis(),
|
||||
vehicleId,
|
||||
signalState
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -40,11 +40,12 @@ class PlatformIntegrationControllerTest {
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.status").value("success"))
|
||||
.andExpect(jsonPath("$.updated").value(3))
|
||||
.andExpect(jsonPath("$.controllableCount").value(1))
|
||||
.andExpect(jsonPath("$.controllableCount").value(2))
|
||||
.andExpect(jsonPath("$.typesCount.WUREN").value(1))
|
||||
.andExpect(jsonPath("$.typesCount.TEQIN").value(1))
|
||||
.andExpect(jsonPath("$.typesCount.HANGKONG").value(1))
|
||||
.andExpect(jsonPath("$.controllableVehicleIDs[0]").value("QN001"));
|
||||
.andExpect(jsonPath("$.controllableVehicleIDs[0]").value("QN001"))
|
||||
.andExpect(jsonPath("$.controllableVehicleIDs[1]").value("TQ001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -27,7 +27,8 @@ class PathConflictDetectionServiceRuntimeConfigTest {
|
||||
mock(ConflictAlertLogRepository.class),
|
||||
mock(ApplicationEventPublisher.class),
|
||||
mock(CoordinateSystemService.class),
|
||||
runtimeStateService
|
||||
runtimeStateService,
|
||||
mock(VehicleCommandService.class)
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
package com.qaup.collision.pathconflict.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qaup.collision.common.model.MovingObject;
|
||||
import com.qaup.collision.pathconflict.model.dto.ConflictAlertEvent;
|
||||
import com.qaup.collision.pathconflict.model.entity.ConflictAlertLog;
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springframework.test.web.client.ExpectedCount.once;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
class VehicleCommandServiceTest {
|
||||
|
||||
private static final String COMMAND_URL = "http://vehicle.example/api/VehicleCommandInfo";
|
||||
|
||||
private VehicleCommandService vehicleCommandService;
|
||||
private MockRestServiceServer server;
|
||||
private PlatformRuntimeStateService runtimeStateService;
|
||||
private GeometryFactory geometryFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
vehicleCommandService = new VehicleCommandService(restTemplate, new ObjectMapper(), runtimeStateService);
|
||||
ReflectionTestUtils.setField(vehicleCommandService, "vehicleApiBaseUrl", "http://vehicle.example");
|
||||
ReflectionTestUtils.setField(vehicleCommandService, "vehicleCommandEndpoint", "/api/VehicleCommandInfo");
|
||||
geometryFactory = new GeometryFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSendWarningOnceAndResumeAfterConflictClears() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("UV-001", PlatformRuntimeStateService.VehicleRegistryType.WUREN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("WARNING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andExpect(jsonPath("$.latitude").value(36.3544))
|
||||
.andExpect(jsonPath("$.longitude").value(120.085))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("UV-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("RESUME"))
|
||||
.andExpect(jsonPath("$.commandReason").value("RESUME_TRAFFIC"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent warningEvent = buildEvent(
|
||||
"UV-001",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.WARNING
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(warningEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(warningEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of());
|
||||
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEscalateToAlertAndParkingOnlyOnceForSpecialVehicle() {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("SP-001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN)
|
||||
));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("SP-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("WARNING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("SP-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("ALERT"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
server.expect(once(), requestTo(COMMAND_URL))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.vehicleID").value("SP-001"))
|
||||
.andExpect(jsonPath("$.commandType").value("PARKING"))
|
||||
.andExpect(jsonPath("$.commandReason").value("AIRCRAFT_CROSSING"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"ok\"}", MediaType.APPLICATION_JSON));
|
||||
|
||||
ConflictAlertEvent warningEvent = buildEvent(
|
||||
"SP-001",
|
||||
MovingObject.MovingObjectType.SPECIAL_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.WARNING
|
||||
);
|
||||
ConflictAlertEvent alertEvent = buildEvent(
|
||||
"SP-001",
|
||||
MovingObject.MovingObjectType.SPECIAL_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.CRITICAL
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(warningEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(alertEvent));
|
||||
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreVehiclesOutsideCommandRegistry() {
|
||||
ConflictAlertEvent warningEvent = buildEvent(
|
||||
"UV-999",
|
||||
MovingObject.MovingObjectType.UNMANNED_VEHICLE,
|
||||
ConflictAlertLog.AlertLevel.WARNING
|
||||
);
|
||||
|
||||
vehicleCommandService.synchronizeConflictCommands(List.of(warningEvent));
|
||||
|
||||
server.verify();
|
||||
}
|
||||
|
||||
private ConflictAlertEvent buildEvent(
|
||||
String vehicleId,
|
||||
MovingObject.MovingObjectType vehicleType,
|
||||
ConflictAlertLog.AlertLevel alertLevel) {
|
||||
|
||||
Point conflictPoint = geometryFactory.createPoint(new Coordinate(120.0850, 36.3544));
|
||||
|
||||
return ConflictAlertEvent.builder()
|
||||
.conflictId(Optional.of(1L))
|
||||
.alertType(Optional.of(
|
||||
alertLevel == ConflictAlertLog.AlertLevel.WARNING
|
||||
? ConflictAlertLog.AlertType.CONFLICT_WARNING
|
||||
: ConflictAlertLog.AlertType.CONFLICT_ALERT
|
||||
))
|
||||
.alertLevel(Optional.of(alertLevel))
|
||||
.message("test")
|
||||
.object1Name(vehicleId)
|
||||
.object1Type(vehicleType)
|
||||
.object2Name("CA1234")
|
||||
.object2Type(MovingObject.MovingObjectType.AIRCRAFT)
|
||||
.conflictPoint(conflictPoint)
|
||||
.object1Distance(12.0)
|
||||
.object2Distance(35.0)
|
||||
.estimatedTimeToConflictObj1(10)
|
||||
.estimatedTimeToConflictObj2(20)
|
||||
.timeGapSeconds(10.0)
|
||||
.eventTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class PlatformRuntimeStateServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldKeepPreviousVehicleRegistrationsOnIncrementalUpdate() {
|
||||
void shouldReplacePreviousVehicleRegistrationsOnFullUpdate() {
|
||||
PlatformRuntimeStateService service = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
|
||||
PlatformRuntimeStateService.VehicleRegistryUpdateResult first = service.updateVehicleRegistry(List.of(
|
||||
@ -22,8 +22,8 @@ class PlatformRuntimeStateServiceTest {
|
||||
));
|
||||
|
||||
assertEquals(3, first.updated());
|
||||
assertEquals(1, first.controllableCount());
|
||||
assertEquals(List.of("QN001"), first.controllableVehicleIDs());
|
||||
assertEquals(2, first.controllableCount());
|
||||
assertEquals(List.of("QN001", "TQ001"), first.controllableVehicleIDs());
|
||||
assertEquals(1, first.typesCount().get("WUREN"));
|
||||
assertEquals(1, first.typesCount().get("TEQIN"));
|
||||
assertEquals(1, first.typesCount().get("HANGKONG"));
|
||||
@ -33,8 +33,12 @@ class PlatformRuntimeStateServiceTest {
|
||||
));
|
||||
|
||||
assertEquals(1, second.updated());
|
||||
assertEquals(2, second.controllableCount());
|
||||
assertEquals(List.of("QN001", "QN002"), second.controllableVehicleIDs());
|
||||
assertEquals(1, second.controllableCount());
|
||||
assertEquals(List.of("QN002"), second.controllableVehicleIDs());
|
||||
assertFalse(service.isRegisteredForCollision("QN001", MovingObject.MovingObjectType.UNMANNED_VEHICLE));
|
||||
assertTrue(service.isRegisteredForCollision("QN002", MovingObject.MovingObjectType.UNMANNED_VEHICLE));
|
||||
assertEquals(List.of("QN002"), service.getControllableVehicleIds());
|
||||
assertEquals(List.of("QN002"), service.getTrafficLightRecipientVehicleIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -68,5 +72,7 @@ class PlatformRuntimeStateServiceTest {
|
||||
assertFalse(service.isRegisteredForCollision("QN001", MovingObject.MovingObjectType.SPECIAL_VEHICLE));
|
||||
assertFalse(service.isRegisteredForCollision("PT001", MovingObject.MovingObjectType.NORMAL_VEHICLE));
|
||||
assertFalse(service.isRegisteredForCollision("UNKNOWN", MovingObject.MovingObjectType.AIRCRAFT));
|
||||
assertEquals(List.of("QN001", "TQ001"), service.getControllableVehicleIds());
|
||||
assertEquals(List.of("QN001", "TQ001"), service.getTrafficLightRecipientVehicleIds());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
package com.qaup.collision.websocket.handler;
|
||||
|
||||
import com.qaup.collision.service.PlatformRuntimeStateService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class VehicleCommandInfoWebSocketHandlerTest {
|
||||
|
||||
private PlatformRuntimeStateService runtimeStateService;
|
||||
private VehicleCommandInfoWebSocketHandler handler;
|
||||
private WebSocketSession session;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
runtimeStateService = new PlatformRuntimeStateService(200.0, 100.0, 40.0);
|
||||
handler = new VehicleCommandInfoWebSocketHandler(runtimeStateService);
|
||||
session = mock(WebSocketSession.class);
|
||||
when(session.getId()).thenReturn("session-1");
|
||||
when(session.isOpen()).thenReturn(true);
|
||||
handler.afterConnectionEstablished(session);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSendSignalOnlyToRegisteredWurenAndTeqin() throws Exception {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("QN001", PlatformRuntimeStateService.VehicleRegistryType.WUREN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("TQ001", PlatformRuntimeStateService.VehicleRegistryType.TEQIN),
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
handler.sendSignalState(1);
|
||||
|
||||
ArgumentCaptor<TextMessage> messageCaptor = ArgumentCaptor.forClass(TextMessage.class);
|
||||
verify(session, times(2)).sendMessage(messageCaptor.capture());
|
||||
List<TextMessage> sentMessages = messageCaptor.getAllValues();
|
||||
assertTrue(sentMessages.stream().anyMatch(message -> message.getPayload().contains("\"vehicleID\": \"QN001\"")));
|
||||
assertTrue(sentMessages.stream().anyMatch(message -> message.getPayload().contains("\"vehicleID\": \"TQ001\"")));
|
||||
assertTrue(sentMessages.stream().noneMatch(message -> message.getPayload().contains("\"vehicleID\": \"AC001\"")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipWhenNoRegisteredRecipients() throws Exception {
|
||||
runtimeStateService.updateVehicleRegistry(List.of(
|
||||
new PlatformRuntimeStateService.VehicleRegistryEntry("AC001", PlatformRuntimeStateService.VehicleRegistryType.HANGKONG)
|
||||
));
|
||||
|
||||
handler.sendSignalState(3);
|
||||
|
||||
verify(session, never()).sendMessage(org.mockito.ArgumentMatchers.any(TextMessage.class));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user