Align conflict alerts with runtime collision distance settings

This commit is contained in:
sladro 2026-03-25 11:23:03 +08:00
parent fb649516ee
commit 9342f8a161
13 changed files with 738 additions and 336 deletions

View File

@ -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 错误响应示例

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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());
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}

View File

@ -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
);
}
/**

View File

@ -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

View File

@ -27,7 +27,8 @@ class PathConflictDetectionServiceRuntimeConfigTest {
mock(ConflictAlertLogRepository.class),
mock(ApplicationEventPublisher.class),
mock(CoordinateSystemService.class),
runtimeStateService
runtimeStateService,
mock(VehicleCommandService.class)
);
@SuppressWarnings("unchecked")

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}