diff --git a/doc/requirement/universal_autonomous_vehicle_api.md b/doc/requirement/universal_autonomous_vehicle_api.md index 7f8ded0b..abd8459e 100644 --- a/doc/requirement/universal_autonomous_vehicle_api.md +++ b/doc/requirement/universal_autonomous_vehicle_api.md @@ -272,7 +272,8 @@ curl -X GET "https://api.example.com/api/v1/vehicles/AV-001/status?fields=vehicl "missionType": "CARGO_TRANSPORT", "startTime": 1736175000000, "estimatedEndTime": 1736178600000, - "progress": 65.5 + "progress": 65.5, + "totalMileage": 1250.8 }, "waypoints": [ { @@ -527,6 +528,7 @@ curl -X GET "https://api.example.com/api/v1/vehicles/AV-001/status?fields=vehicl - **startTime**: 开始时间戳 [可选] - **estimatedEndTime**: 预计结束时间戳 [可选] - **progress**: 任务进度百分比 [可选] +- **totalMileage**: 累计行驶里程 (米) [可选] #### 路径点 (waypoints) [可选] - **waypointId**: 路径点ID [可选] diff --git a/doc/requirement/universal_autonomous_vehicle_api_min_required.md b/doc/requirement/universal_autonomous_vehicle_api_min_required.md index ef2b9c78..d147ed64 100644 --- a/doc/requirement/universal_autonomous_vehicle_api_min_required.md +++ b/doc/requirement/universal_autonomous_vehicle_api_min_required.md @@ -95,6 +95,20 @@ - wifiStatus: CONNECTED | DISCONNECTED | FAULT - cloudConnectivity: ONLINE | OFFLINE | FAULT +10) missionContext.currentMission +- missionId: string +- missionType: string +- startTime: number (ms, UTC) +- estimatedEndTime: number (ms, UTC) +- progress: number (0-100) +- totalMileage: number (m) + +11) missionContext.waypoints +- waypointId: string +- latitude: number +- longitude: number +- status: PENDING | COMPLETED | SKIPPED + --- ## 3. 统一要求 @@ -213,6 +227,30 @@ curl -sS -X GET "${BASE_URL}/api/v1/vehicles/${VEHICLE_ID}/status" \ "cellularSignalStrength": -65, "wifiStatus": "CONNECTED", "cloudConnectivity": "ONLINE" + }, + "missionContext": { + "currentMission": { + "missionId": "MISSION_001", + "missionType": "CARGO_TRANSPORT", + "startTime": 1736175000000, + "estimatedEndTime": 1736178600000, + "progress": 65.5, + "totalMileage": 1250.8 + }, + "waypoints": [ + { + "waypointId": "WP_001", + "latitude": 36.354068, + "longitude": 120.083410, + "status": "COMPLETED" + }, + { + "waypointId": "WP_002", + "latitude": 36.355123, + "longitude": 120.084567, + "status": "PENDING" + } + ] } } } @@ -346,6 +384,46 @@ curl -sS -X GET "${BASE_URL}/api/v1/vehicles/${VEHICLE_ID}/status" \ - communicationStatus.cloudConnectivity - 类型:enum(ONLINE|OFFLINE|FAULT);必填:是 +- missionContext.currentMission.missionId + - 类型:string;必填:是 + - 说明:当前任务的唯一标识符 + +- missionContext.currentMission.missionType + - 类型:string;必填:是 + - 说明:任务类型,如 CARGO_TRANSPORT、PATROL_TRANSPORT 等 + +- missionContext.currentMission.startTime + - 类型:number(ms,UTC);必填:是 + - 说明:任务开始时间 + +- missionContext.currentMission.estimatedEndTime + - 类型:number(ms,UTC);必填:是 + - 说明:预计任务结束时间 + +- missionContext.currentMission.progress + - 类型:number;必填:是;范围:0~100(%) + - 说明:当前任务执行进度百分比 + +- missionContext.currentMission.totalMileage + - 类型:number;必填:是;单位:m + - 说明:累计行驶里程(米) + +- missionContext.waypoints.waypointId + - 类型:string;必填:是 + - 说明:路径点唯一标识符 + +- missionContext.waypoints.latitude + - 类型:number;必填:是;单位:WGS84 度 + - 说明:路径点纬度 + +- missionContext.waypoints.longitude + - 类型:number;必填:是;单位:WGS84 度 + - 说明:路径点经度 + +- missionContext.waypoints.status + - 类型:enum(PENDING|COMPLETED|SKIPPED);必填:是 + - 说明:路径点状态,PENDING=待到达,COMPLETED=已完成,SKIPPED=已跳过 + --- ## 8. 兼容性与扩展 diff --git a/qaup-admin/src/main/resources/application-prod.yml b/qaup-admin/src/main/resources/application-prod.yml index 1886bc55..5fd8079d 100644 --- a/qaup-admin/src/main/resources/application-prod.yml +++ b/qaup-admin/src/main/resources/application-prod.yml @@ -192,11 +192,10 @@ data: username: dianxin password: dianxin@123 vehicle-api: - base-url: http://localhost:8090 + base-url: http://localhost:8091 endpoints: - vehicle-location: /api/VehicleLocationInfo - vehicle-state: /api/VehicleStateInfo vehicle-command: /api/VehicleCommandInfo + universal-status: /api/v1/vehicles/{vehicleId}/status timeout: 1000 retry-attempts: 3 unmanned-vehicle: diff --git a/qaup-admin/src/main/resources/application.yml b/qaup-admin/src/main/resources/application.yml index acd68f26..655f1f0f 100644 --- a/qaup-admin/src/main/resources/application.yml +++ b/qaup-admin/src/main/resources/application.yml @@ -221,8 +221,6 @@ data: vehicle-api: base-url: http://localhost:8091 endpoints: - vehicle-location: /api/VehicleLocationInfo - vehicle-state: /api/VehicleStateInfo vehicle-command: /api/VehicleCommandInfo # 通用车辆状态API端点(符合universal_autonomous_vehicle_api规范) universal-status: /api/v1/vehicles/{vehicleId}/status diff --git a/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java b/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java index b95da962..d74ea7eb 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java +++ b/qaup-collision/src/main/java/com/qaup/collision/common/model/UnmannedVehicle.java @@ -45,6 +45,36 @@ public class UnmannedVehicle extends MovingObject { * 目标位置 */ private Point targetPosition; + + /** + * 任务类型 + */ + private String missionType; + + /** + * 任务开始时间戳 + */ + private Long missionStartTime; + + /** + * 预计任务结束时间戳 + */ + private Long estimatedEndTime; + + /** + * 任务进度百分比 (0-100) + */ + private Double progress; + + /** + * 累计行驶里程(米) + */ + private Double totalMileage; + + /** + * 路径点列表 + */ + private java.util.List waypoints; /** * 无人车运行状态枚举 @@ -69,6 +99,29 @@ public class UnmannedVehicle extends MovingObject { PAUSED, // 暂停 CANCELLED // 已取消 } + + /** + * 路径点状态枚举 + */ + public enum WaypointStatus { + PENDING, // 待到达 + COMPLETED, // 已完成 + SKIPPED // 已跳过 + } + + /** + * 路径点信息 + */ + @Data + @lombok.Builder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + public static class WaypointInfo { + private String waypointId; + private Double latitude; + private Double longitude; + private WaypointStatus status; + } /** * 是否可以接收新指令 diff --git a/qaup-collision/src/main/java/com/qaup/collision/controller/UnmannedVehicleController.java b/qaup-collision/src/main/java/com/qaup/collision/controller/UnmannedVehicleController.java index 647a08e3..379a681c 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/controller/UnmannedVehicleController.java +++ b/qaup-collision/src/main/java/com/qaup/collision/controller/UnmannedVehicleController.java @@ -1,10 +1,7 @@ package com.qaup.collision.controller; import com.qaup.collision.common.model.dto.Response; -import com.qaup.collision.common.model.spatial.VehicleLocation; import com.qaup.collision.datacollector.model.dto.VehicleCommand; -import com.qaup.collision.datacollector.model.dto.VehicleStateInfo; -import com.qaup.collision.datacollector.model.dto.VehicleStateRequest; import com.qaup.collision.datacollector.service.UnmannedVehicleControlService; import org.slf4j.Logger; @@ -15,7 +12,6 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; -import java.util.List; /** * 无人车控制接口控制器 @@ -65,68 +61,4 @@ public class UnmannedVehicleController { .body(Response.error("控制指令执行失败: " + e.getMessage())); } } - - /** - * 无人车位置上报接口 - * 查询指定车辆或所有无人车的当前位置信息 - * - * @param vehicleId 车辆ID(可选,为空时返回所有无人车位置) - * @return 车辆位置信息列表 - */ - @GetMapping("/VehicleLocationInfo") - public ResponseEntity>> getVehicleLocationInfo( - @RequestParam(required = false) String vehicleId) { - - logger.info("查询无人车位置信息: vehicleId={}", vehicleId); - - try { - List locations = unmannedVehicleControlService.getVehicleLocations(vehicleId); - - logger.info("查询无人车位置信息成功: vehicleId={}, count={}", vehicleId, locations.size()); - - return ResponseEntity.ok(Response.success("位置信息查询成功", locations)); - - } catch (Exception e) { - logger.error("查询无人车位置信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e); - - return ResponseEntity.badRequest() - .body(Response.error("位置信息查询失败: " + e.getMessage())); - } - } - - /** - * 无人车状态查询接口 - * 查询指定车辆的状态信息 - * - * @param request 状态查询请求对象 - * @return 车辆状态信息 - */ - @PostMapping("/VehicleStateInfo") - public ResponseEntity> getVehicleStateInfo( - @Valid @RequestBody VehicleStateRequest request) { - - logger.info("查询无人车状态信息: vehicleId={}", request.getVehicleId()); - - try { - // 将Long类型的vehicleId转换为String类型 - VehicleStateInfo vehicleState = unmannedVehicleControlService.getVehicleState(String.valueOf(request.getVehicleId())); - - if (vehicleState != null) { - logger.info("查询无人车状态信息成功: vehicleId={}", request.getVehicleId()); - - return ResponseEntity.ok(Response.success("状态信息查询成功", vehicleState)); - } else { - logger.warn("未找到指定车辆的状态信息: vehicleId={}", request.getVehicleId()); - - return ResponseEntity.badRequest() - .body(Response.error("未找到指定车辆的状态信息")); - } - - } catch (Exception e) { - logger.error("查询无人车状态信息失败: vehicleId={}, error={}", request.getVehicleId(), e.getMessage(), e); - - return ResponseEntity.badRequest() - .body(Response.error("状态信息查询失败: " + e.getMessage())); - } - } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java index 5c99e886..bbf68345 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/dao/DataCollectorDao.java @@ -3,8 +3,6 @@ package com.qaup.collision.datacollector.dao; import com.qaup.collision.common.model.Aircraft; import com.qaup.collision.common.model.AirportVehicle; -import com.qaup.collision.common.model.UnmannedVehicle; -import com.qaup.collision.common.model.MovingObject.MovingObjectType; import com.qaup.collision.common.model.dto.Response; import com.qaup.collision.datacollector.service.AuthService; import com.qaup.collision.datacollector.dto.AircraftRouteDTO; @@ -53,10 +51,6 @@ public class DataCollectorDao { @Value("${data.collector.vehicle-api.base-url}") private String vehicleBaseUrl; - @Value("${data.collector.vehicle-api.endpoints.vehicle-location}") - private String vehicleLocationEndpoint; - - private final RestTemplate restTemplate; private final AuthService authService; private final GeometryFactory geometryFactory; @@ -163,61 +157,6 @@ public class DataCollectorDao { return Collections.emptyList(); } - /** - * 获取无人车位置信息 - * 数据来源:第2章 无人车位置上报 (/api/VehicleLocationInfo) - * 说明:此接口专门用于获取无人车的实时位置数据,用于数据持久化 - * - * @return 无人车位置信息列表 - */ - public List getVehicleLocationInfo() { - try { - String url = UriComponentsBuilder - .fromUriString(vehicleBaseUrl) - .path(vehicleLocationEndpoint) - .toUriString(); - - log.debug("正在从 {} 获取无人车位置信息", url); - - HttpHeaders headers = new HttpHeaders(); - HttpEntity requestEntity = new HttpEntity<>(headers); - - ResponseEntity> response = restTemplate.exchange( - url, - HttpMethod.GET, - requestEntity, - new ParameterizedTypeReference>() {} - ); - - if (response.getStatusCode().is2xxSuccessful()) { - List rawDataList = response.getBody(); - if (rawDataList != null) { - List processedUnmannedVehicles = rawDataList.stream().map(rawData -> { - if (rawData.getVehicleID() == null || rawData.getLongitude() == null || rawData.getLatitude() == null) { - log.warn("原始无人车数据缺失必要字段 (vehicleID, longitude, latitude),跳过处理: {}", rawData); - return null; - } - UnmannedVehicle unmannedVehicle = new UnmannedVehicle(); - unmannedVehicle.setObjectId(rawData.getVehicleID()); - unmannedVehicle.setObjectName(rawData.getVehicleID()); // 使用 vehicleID 作为 objectName - unmannedVehicle.setCurrentPosition(geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(rawData.getLongitude(), rawData.getLatitude()))); - unmannedVehicle.setCurrentSpeed(rawData.getSpeed() != null ? rawData.getSpeed() * 3.6 : 0.0); // m/s to km/h - unmannedVehicle.setCurrentHeading(rawData.getDirection() != null ? Math.toDegrees(rawData.getDirection()) : 0.0); // 弧度 to 角度 - unmannedVehicle.setAltitude(0.0); // 假设默认高度为0,如果API有提供可以设置 - unmannedVehicle.setObjectType(MovingObjectType.UNMANNED_VEHICLE); // 显式设置对象类型 - return unmannedVehicle; - }).filter(java.util.Objects::nonNull).collect(Collectors.toList()); - - log.info("成功获取无人车定位信息,数量: {}", processedUnmannedVehicles.size()); - return processedUnmannedVehicles; - } - } - } catch (Exception e) { - log.error("获取无人车定位信息时发生异常", e); - return Collections.emptyList(); - } - return Collections.emptyList(); - } // 内部 DTOs,精确匹配外部 API 接口返回的 JSON 结构 @@ -252,26 +191,6 @@ public class DataCollectorDao { private Long time; } - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonIgnoreProperties(ignoreUnknown = true) - private static class ExternalUnmannedVehicleData { - @JsonProperty("transId") - private String transId; - @JsonProperty("timestamp") - private Long timestamp; - @JsonProperty("vehicleID") - private String vehicleID; - @JsonProperty("latitude") - private Double latitude; - @JsonProperty("longitude") - private Double longitude; - @JsonProperty("speed") - private Double speed; - @JsonProperty("direction") - private Double direction; - } // 新增航空器路由和状态数据采集方法 @@ -576,12 +495,15 @@ public class DataCollectorDao { if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { com.qaup.collision.datacollector.model.dto.UniversalApiResponse apiResponse = response.getBody(); - if (apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) { + // 检查 apiResponse 是否为 null,避免空指针异常 + if (apiResponse != null && apiResponse.getCode() != null && apiResponse.getCode() == 200 && apiResponse.getData() != null) { log.debug("成功获取车辆 {} 的通用状态数据", vehicleId); return apiResponse.getData(); } else { log.warn("API返回错误: 车辆={}, code={}, message={}", - vehicleId, apiResponse.getCode(), apiResponse.getMessage()); + vehicleId, + apiResponse != null ? apiResponse.getCode() : null, + apiResponse != null ? apiResponse.getMessage() : "响应为空"); return null; } } else { diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/MissionContextDTO.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/MissionContextDTO.java new file mode 100644 index 00000000..3b75b336 --- /dev/null +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/MissionContextDTO.java @@ -0,0 +1,100 @@ +package com.qaup.collision.datacollector.model.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; +import java.util.List; + +/** + * 任务上下文DTO + * 符合universal_autonomous_vehicle_api规范的任务上下文数据结构 + * + * @author AI Assistant + * @version 1.0 + * @since 2025-01-19 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class MissionContextDTO { + + /** + * 当前任务信息 + */ + private CurrentMissionDTO currentMission; + + /** + * 路径点列表 + */ + private List waypoints; + + /** + * 当前任务DTO + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CurrentMissionDTO { + /** + * 任务ID + */ + private String missionId; + + /** + * 任务类型 + */ + private String missionType; + + /** + * 开始时间戳 (ms, UTC) + */ + private Long startTime; + + /** + * 预计结束时间戳 (ms, UTC) + */ + private Long estimatedEndTime; + + /** + * 任务进度百分比 (0-100) + */ + private Double progress; + + /** + * 累计行驶里程(米) + */ + private Double totalMileage; + } + + /** + * 路径点DTO + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class WaypointDTO { + /** + * 路径点ID + */ + private String waypointId; + + /** + * 纬度 + */ + private Double latitude; + + /** + * 经度 + */ + private Double longitude; + + /** + * 状态 (PENDING, COMPLETED, SKIPPED) + */ + private String status; + } +} \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/UniversalVehicleStatusDTO.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/UniversalVehicleStatusDTO.java index c273ffd6..41c17c98 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/UniversalVehicleStatusDTO.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/model/dto/UniversalVehicleStatusDTO.java @@ -68,4 +68,11 @@ public class UniversalVehicleStatusDTO { * 包含:v2xStatus, cellularSignalStrength, wifiStatus, cloudConnectivity */ private CommunicationStatusDTO communicationStatus; + + /** + * 任务上下文字段组 + * 包含:currentMission (missionId, missionType, startTime, estimatedEndTime, progress, totalMileage), + * waypoints (waypointId, latitude, longitude, status) + */ + private MissionContextDTO missionContext; } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java index d2fdb9a3..270e5267 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/DataCollectorService.java @@ -5,15 +5,12 @@ import com.qaup.collision.common.model.MovingObject.MovingObjectType; import com.qaup.collision.common.model.Aircraft; import com.qaup.collision.common.model.AirportVehicle; import com.qaup.collision.common.model.UnmannedVehicle; -import com.qaup.collision.common.model.AircraftRoute; import com.qaup.collision.common.service.VehicleLocationService; import com.qaup.collision.datacollector.dao.DataCollectorDao; -import com.qaup.collision.datacollector.dto.AircraftRouteDTO; -import com.qaup.collision.datacollector.dto.AircraftStatusDTO; import com.qaup.collision.datacollector.dto.FlightNotificationDTO; import com.qaup.collision.common.model.FlightNotification; +import com.qaup.collision.datacollector.model.dto.MissionContextDTO; import com.qaup.collision.datacollector.filter.VehicleLocationFilter; -import com.qaup.collision.websocket.event.AircraftRouteUpdateEvent; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; @@ -23,12 +20,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import org.springframework.context.ApplicationEventPublisher; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -36,7 +31,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.PrecisionModel; -import com.qaup.collision.datacollector.util.RouteGeometryProcessor; /** * 数据采集服务 - 重构版本 @@ -71,8 +65,6 @@ public class DataCollectorService { @Value("${data.collector.detection.interval:1000}") private long detectionInterval; - @Value("${data.collector.route.cache-expiry-hours:2}") - private int routeCacheExpiryHours; @Value("${data.collector.route.periodic-collection-enabled:false}") private boolean periodicRouteCollectionEnabled; @@ -83,18 +75,9 @@ public class DataCollectorService { @Autowired private VehicleLocationService vehicleLocationService; - @Autowired - private ApplicationEventPublisher eventPublisher; - @Autowired private com.qaup.collision.dataprocessing.service.DataProcessingService dataProcessingService; // 注入数据处理服务 - @Autowired - private RouteGeometryProcessor routeGeometryProcessor; // 注入路由几何处理器 - - @Autowired - private RoutePersistenceService routePersistenceService; // 注入路由持久化服务 - @Autowired private VehicleLocationFilter vehicleLocationFilter; // 注入车辆位置过滤器 @@ -106,8 +89,6 @@ public class DataCollectorService { // 用于缓存所有活跃的MovingObject的最新状态 private final Map activeMovingObjectsCache = new ConcurrentHashMap<>(); - // 路由获取状态缓存:Key=flightNo:type:time, Value=获取时间戳 - private final Map routeRetrievalCache = new ConcurrentHashMap<>(); // 航班通知缓存:Key=flightNo:type, Value=最新的航班通知 private final Map flightNotificationCache = new ConcurrentHashMap<>(); @@ -119,99 +100,6 @@ public class DataCollectorService { log.info("DataCollectorService 初始化完成,缓存引用已传递给DataProcessingService"); } - /** - * 将AircraftRouteDTO转换为AircraftRoute对象 - * 使用JTS将多个LineString段合并为单一连续路径 - */ - private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) { - if (routeDTO == null || routeDTO.getGeoPath() == null) { - return null; - } - - try { - // 创建路由段列表 - List routeSegments = new ArrayList<>(); - List lineStringSegments = new ArrayList<>(); - - if (routeDTO.getGeoPath().getFeatures() != null) { - for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) { - if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) { - // 转换坐标格式:从List>到List - List points = feature.getGeometry().getCoordinates().stream() - .map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1)))) - .collect(java.util.stream.Collectors.toList()); - - // 创建路由段 - AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder() - .code(feature.getProperties() != null ? feature.getProperties().getCode() : "") - .coordinates(points) - .build(); - routeSegments.add(segment); - - // 创建LineString段用于合并 - if (points.size() >= 2) { - org.locationtech.jts.geom.Coordinate[] coords = points.stream() - .map(point -> point.getCoordinate()) - .toArray(org.locationtech.jts.geom.Coordinate[]::new); - org.locationtech.jts.geom.LineString lineString = geometryFactory.createLineString(coords); - lineStringSegments.add(lineString); - } - } - } - } - - // 使用JTS将多个LineString段合并为单一连续路径 - org.locationtech.jts.geom.LineString mergedGeometry = null; - if (!lineStringSegments.isEmpty()) { - mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments); - - if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) { - // 可选:简化路径以减少冗余点(容差:1米) - mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0); - log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点", - lineStringSegments.size(), mergedGeometry.getNumPoints()); - } else { - log.warn("路由段合并失败或结果无效"); - mergedGeometry = null; - } - } - - return AircraftRoute.builder() - .type(routeDTO.getType()) - .status(routeDTO.getStatus()) - .codes(routeDTO.getCodes()) - .geometry(mergedGeometry) // 使用合并后的单一LineString - .routeSegments(routeSegments) - .build(); - - } catch (Exception e) { - log.error("转换航空器路由数据失败", e); - return null; - } - } - - - /** - * 保存航空器路由到数据库(独立于缓存) - */ - private void saveAircraftRouteToDatabase(String flightNo, AircraftRoute route) { - try { - log.info("开始保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType()); - boolean saveSuccess = routePersistenceService.saveAircraftRoute(flightNo, route); - - if (saveSuccess) { - log.info("成功保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType()); - - // TODO: 保存到路由分配表 - // saveRouteAssignment(flightNo, route); - - } else { - log.warn("保存航空器路由到数据库失败: flightNo={}, type={}", flightNo, route.getType()); - } - } catch (Exception e) { - log.error("保存航空器路由到数据库异常: flightNo={}, type={}", flightNo, route.getType(), e); - } - } /** @@ -233,10 +121,6 @@ public class DataCollectorService { log.info("采集到 {} 条航空器数据,用于实时处理", newAircrafts.size()); - // List activeMovingObjects = new ArrayList<>(); // 移除此行 - - // 航空器数据仅用于实时处理,不存储到数据库 - // 数据处理完成后发布WebSocket事件进行实时推送 for (Aircraft aircraft : newAircrafts) { try { if (aircraft.getCurrentPosition() == null) { @@ -259,7 +143,6 @@ public class DataCollectorService { .altitude(aircraft.getAltitude()) .build(); - // 将最新数据更新到缓存(不发送WebSocket消息,统一在周期性检测中发送) activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject); log.debug("处理航空器数据并更新缓存: (航班号: {}, 位置: {}, {}, 速度: {})", @@ -272,24 +155,6 @@ public class DataCollectorService { log.error("处理航空器数据异常: objectId={}", aircraft.getObjectId(), e); // Changed from flightNo } } - - // 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理) - // if (!activeMovingObjects.isEmpty()) { - // pathConflictDetectionService.detectPathConflicts(activeMovingObjects); - // } - - // 执行实时违规检测 (临时移除,将在新的定时任务中统一处理) - // for (MovingObject movingObject : activeMovingObjects) { - // VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject); - // if (tempVehicleLocation != null) { - // List violations = realTimeViolationDetector.detectViolations(tempVehicleLocation); - // if (!violations.isEmpty()) { - // log.warn("检测到航空器违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size()); - // // 如果需要,可以在这里进一步处理这些违规事件,例如持久化或发送告警 - // // realTimeViolationDetector.processBatchViolationEvents(violations); - // } - // } - // } log.info("航空器数据处理和事件发布完成,处理数量: {}", newAircrafts.size()); @@ -376,24 +241,6 @@ public class DataCollectorService { log.error("处理机场车辆数据异常: objectId={}", vehicle.getObjectId(), e); } } - - // 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理) - // if (!activeMovingObjects.isEmpty()) { - // pathConflictDetectionService.detectPathConflicts(activeMovingObjects); - // } - - // 执行实时违规检测 (临时移除,将在新的定时任务中统一处理) - // for (MovingObject movingObject : activeMovingObjects) { - // VehicleLocation tempVehicleLocation = createTemporaryVehicleLocationForDetection(movingObject); - // if (tempVehicleLocation != null) { - // List violations = realTimeViolationDetector.detectViolations(tempVehicleLocation); - // if (!violations.isEmpty()) { - // log.warn("检测到机场车辆违规: objectId={}, 违规数={}", movingObject.getObjectId(), violations.size()); - // // 如果需要,可以在这里进一步处理这些违规事件,例如持久化或发送告警 - // // realTimeViolationDetector.processBatchViolationEvents(violations); - // } - // } - // } log.info("机场车辆数据处理和事件发布完成,处理数量: {}", filteredVehicles.size()); @@ -404,13 +251,13 @@ public class DataCollectorService { /** * 定时采集无人车数据 (外部API) - * - * 数据来源:第2章 无人车位置上报 (/api/VehicleLocationInfo) - * 说明:仅传递目前无人车平台已接入的无人车位置数据 + * + * 数据来源:通用无人车状态API (/api/v1/vehicles/{vehicleId}/status) + * 说明:采集完整的无人车状态数据,包含位置和任务上下文信息 * 重构说明: - * - 无人车数据仅用于实时处理,不存储到数据库 - * - 数据采集后直接用于碰撞检测等实时计算 - * - 不进行数据持久化 + * - 使用通用状态API替代基础位置API,获取更完整的数据 + * - 包含missionContext任务信息,支持里程和路径点数据 + * - 数据仅用于实时处理,不存储到数据库 */ @Scheduled(fixedRateString = "${data.collector.interval}") @Async // 异步执行 @@ -418,88 +265,150 @@ public class DataCollectorService { if (collectorDisabled) { return; } - + try { - List unmannedVehicles = dataCollectorDao.getVehicleLocationInfo(); - if (unmannedVehicles.isEmpty()) { - log.debug("未获取到无人车数据"); + // 获取当前活跃的无人车列表(从之前的缓存或配置中获取) + java.util.Set knownVehicleIds = getKnownUnmannedVehicleIds(); + + if (knownVehicleIds.isEmpty()) { + log.debug("当前没有已知的无人车ID,跳过数据采集"); return; } - - log.info("采集到 {} 条无人车数据", unmannedVehicles.size()); - // 将采集到的无人车数据转换为MovingObject并添加到缓存 - // List unmannedVehicleLocations = new ArrayList<>(); // 移除此行 - - for (UnmannedVehicle unmannedVehicle : unmannedVehicles) { + log.info("开始采集 {} 辆无人车的完整状态数据", knownVehicleIds.size()); + int successCount = 0; + + for (String vehicleId : knownVehicleIds) { try { - if (unmannedVehicle.getCurrentPosition() == null) { - log.warn("无人车 {} 位置信息缺失,跳过处理。", unmannedVehicle.getObjectId()); - continue; // 跳过此无人车 + // 调用通用状态API获取完整数据 + com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData = + dataCollectorDao.getUniversalVehicleStatus(vehicleId); + + if (statusData == null || statusData.getMotionStatus() == null) { + log.debug("无人车 {} 未返回有效状态数据,跳过处理", vehicleId); + continue; } - Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate( - unmannedVehicle.getCurrentPosition().getX(), - unmannedVehicle.getCurrentPosition().getY() - )); + // 提取位置信息 + if (statusData.getMotionStatus().getPosition() == null) { + log.warn("无人车 {} 位置信息缺失,跳过处理", vehicleId); + continue; + } - // 数据采集阶段:只采集和缓存位置数据,不进行任何计算 - MovingObject movingObject = MovingObject.builder() - .objectId(unmannedVehicle.getObjectId()) + Double latitude = statusData.getMotionStatus().getPosition().getLatitude(); + Double longitude = statusData.getMotionStatus().getPosition().getLongitude(); + + if (latitude == null || longitude == null) { + log.warn("无人车 {} 经纬度信息缺失,跳过处理", vehicleId); + continue; + } + + Point currentPosition = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude)); + + // 创建增强的UnmannedVehicle对象 + var vehicleBuilder = UnmannedVehicle.builder() + .objectId(vehicleId) .objectType(MovingObjectType.UNMANNED_VEHICLE) - .objectName(unmannedVehicle.getObjectName()) + .objectName(vehicleId) .currentPosition(currentPosition) .currentSpeed(null) // 不在采集阶段计算速度 .currentHeading(null) // 不在采集阶段计算方向 - .altitude(unmannedVehicle.getAltitude()) - .build(); + .altitude(0.0); // 默认高度 - log.debug("在DataCollectorService中,MovingObject的速度: {}", movingObject.getCurrentSpeed()); + // 提取任务上下文信息 + if (statusData.getMissionContext() != null) { + MissionContextDTO missionContext = statusData.getMissionContext(); - // 注意:电子围栏检测已移至周期性检测方法中,以降低检测频率 - // 仅在此处更新缓存,实际检测在performPeriodicViolationDetection()中执行 + if (missionContext.getCurrentMission() != null) { + MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission(); + vehicleBuilder + .missionId(currentMission.getMissionId()) + .missionType(currentMission.getMissionType()) + .missionStartTime(currentMission.getStartTime()) + .estimatedEndTime(currentMission.getEstimatedEndTime()) + .progress(currentMission.getProgress()) + .totalMileage(currentMission.getTotalMileage()); + } - // 将最新数据更新到缓存(不保存到数据库,统一在周期性检测中保存) - activeMovingObjectsCache.put(movingObject.getObjectId(), movingObject); - - log.debug("处理无人车数据并更新缓存: (车牌号: {}, 位置: {}, {}, 速度: {})", - unmannedVehicle.getObjectId(), + // 提取路径点信息 + if (missionContext.getWaypoints() != null) { + java.util.List waypoints = missionContext.getWaypoints().stream() + .map(wp -> UnmannedVehicle.WaypointInfo.builder() + .waypointId(wp.getWaypointId()) + .latitude(wp.getLatitude()) + .longitude(wp.getLongitude()) + .status(UnmannedVehicle.WaypointStatus.valueOf(wp.getStatus())) + .build()) + .collect(java.util.stream.Collectors.toList()); + vehicleBuilder.waypoints(waypoints); + } + } + + // 提取电池信息 + if (statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null) { + Double chargeLevel = statusData.getBatteryStatus().getMainBattery().getChargeLevel(); + if (chargeLevel != null) { + vehicleBuilder.batteryLevel(chargeLevel.intValue()); + } + } + + // 设置车辆运行状态 + if (statusData.getOperationalStatus() != null) { + String systemHealth = statusData.getOperationalStatus().getSystemHealth(); + if ("HEALTHY".equals(systemHealth)) { + vehicleBuilder.vehicleStatus(UnmannedVehicle.VehicleStatus.WORKING); + } else { + vehicleBuilder.vehicleStatus(UnmannedVehicle.VehicleStatus.ERROR); + } + } + + UnmannedVehicle enhancedUnmannedVehicle = vehicleBuilder.build(); + + // 将最新数据更新到缓存 + activeMovingObjectsCache.put(enhancedUnmannedVehicle.getObjectId(), enhancedUnmannedVehicle); + successCount++; + + log.debug("处理无人车完整状态数据并更新缓存: (车辆ID: {}, 位置: {}, {}, 任务ID: {}, 里程: {}米, 电量: {}%)", + vehicleId, currentPosition.getX(), currentPosition.getY(), - movingObject.getCurrentSpeed()); + enhancedUnmannedVehicle.getMissionId(), + enhancedUnmannedVehicle.getTotalMileage(), + enhancedUnmannedVehicle.getBatteryLevel()); } catch (Exception e) { - log.error("处理无人车数据异常: objectId={}", unmannedVehicle.getObjectId(), e); + log.error("处理无人车状态数据异常: vehicleId={}", vehicleId, e); } } - // 执行路径冲突检测 (临时移除,将在新的定时任务中统一处理) - // if (!unmannedVehicleLocations.isEmpty()) { - // pathConflictDetectionService.detectPathConflicts(unmannedVehicleLocations.stream() - // .map(v -> MovingObject.builder() - // .objectId(String.valueOf(v.getVehicleId())) - // .objectType(v.getVehicleType()) - // .objectName(v.getVehicleLicense()) - // .currentPosition(v.getLocation()) - // .currentSpeed(v.getSpeed()) - // .currentHeading(v.getHeading()) - // .altitude(v.getAltitude()) - // .build()) - // .collect(Collectors.toList())); - // } - - // 执行实时违规检测 (临时移除,将在新的定时任务中统一处理) - // if (!unmannedVehicleLocations.isEmpty()) { - // realTimeViolationDetector.detectBatchViolations(unmannedVehicleLocations); - // } - - log.info("无人车数据处理和事件发布完成,处理数量: {}", unmannedVehicles.size()); + log.info("无人车完整状态数据采集完成,成功处理: {}/{}", successCount, knownVehicleIds.size()); } catch (Exception e) { log.error("采集无人车数据异常", e); } } + /** + * 获取已知的无人车ID列表 + * 可以从配置、缓存或其他数据源获取 + */ + private java.util.Set getKnownUnmannedVehicleIds() { + java.util.Set vehicleIds = new java.util.HashSet<>(); + + // 从当前缓存中获取已存在的无人车ID + activeMovingObjectsCache.values().stream() + .filter(obj -> obj.getObjectType() == MovingObjectType.UNMANNED_VEHICLE) + .forEach(obj -> vehicleIds.add(obj.getObjectId())); + + // 如果缓存为空,使用默认的无人车ID(与mock服务对应) + if (vehicleIds.isEmpty()) { + vehicleIds.add("鲁B567"); + vehicleIds.add("鲁B579"); + } + + return vehicleIds; + } + /** * 定时采集通用车辆状态数据 (符合universal_autonomous_vehicle_api规范) * @@ -567,14 +476,14 @@ public class DataCollectorService { } /** - * 缓存通用车辆状态数据 + * 缓存通用无人车状态数据 * 将状态数据存储到专门的缓存中供DataProcessingService处理 + * 同时将missionContext信息更新到activeMovingObjectsCache中的无人车对象 */ private void cacheUniversalVehicleStatus(String vehicleId, com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData) { // 使用专门的缓存存储通用状态数据 - // 注意:这里不影响activeMovingObjectsCache中的位置数据,两者独立存储 String cacheKey = "universal_status_" + vehicleId; - + // 创建带时间戳的状态数据包装器 UniversalVehicleStatusCacheEntry cacheEntry = UniversalVehicleStatusCacheEntry.builder() .vehicleId(vehicleId) @@ -582,12 +491,63 @@ public class DataCollectorService { .timestamp(System.currentTimeMillis()) .dataSource("COLLECTOR") .build(); - - // 存储到缓存中(这里简化为使用activeMovingObjectsCache的扩展,实际可以使用Redis等) - // 临时解决方案:使用一个Map来存储状态数据 + + // 存储到通用状态缓存中 getUniversalStatusCache().put(cacheKey, cacheEntry); - - log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}", vehicleId, cacheKey); + + // 如果存在missionContext数据,更新到activeMovingObjectsCache中的无人车对象 + if (statusData.getMissionContext() != null) { + updateUnmannedVehicleMissionContext(vehicleId, statusData.getMissionContext()); + } + + log.debug("缓存通用车辆状态数据: vehicleId={}, cacheKey={}, 包含任务上下文: {}", + vehicleId, cacheKey, statusData.getMissionContext() != null); + } + + /** + * 更新无人车的任务上下文信息 + */ + private void updateUnmannedVehicleMissionContext(String vehicleId, MissionContextDTO missionContext) { + MovingObject existingObject = activeMovingObjectsCache.get(vehicleId); + + if (existingObject instanceof UnmannedVehicle) { + UnmannedVehicle unmannedVehicle = (UnmannedVehicle) existingObject; + + // 更新任务上下文信息 + if (missionContext.getCurrentMission() != null) { + MissionContextDTO.CurrentMissionDTO currentMission = missionContext.getCurrentMission(); + unmannedVehicle.setMissionId(currentMission.getMissionId()); + unmannedVehicle.setMissionType(currentMission.getMissionType()); + unmannedVehicle.setMissionStartTime(currentMission.getStartTime()); + unmannedVehicle.setEstimatedEndTime(currentMission.getEstimatedEndTime()); + unmannedVehicle.setProgress(currentMission.getProgress()); + unmannedVehicle.setTotalMileage(currentMission.getTotalMileage()); + } + + // 更新路径点信息 + if (missionContext.getWaypoints() != null) { + java.util.List waypoints = missionContext.getWaypoints().stream() + .map(wp -> UnmannedVehicle.WaypointInfo.builder() + .waypointId(wp.getWaypointId()) + .latitude(wp.getLatitude()) + .longitude(wp.getLongitude()) + .status(UnmannedVehicle.WaypointStatus.valueOf(wp.getStatus())) + .build()) + .collect(java.util.stream.Collectors.toList()); + unmannedVehicle.setWaypoints(waypoints); + } + + // 更新缓存 + activeMovingObjectsCache.put(vehicleId, unmannedVehicle); + + log.debug("更新无人车任务上下文: vehicleId={}, 任务ID={}, 里程={}米, 路径点数量={}", + vehicleId, + unmannedVehicle.getMissionId(), + unmannedVehicle.getTotalMileage(), + unmannedVehicle.getWaypoints() != null ? unmannedVehicle.getWaypoints().size() : 0); + } else { + log.debug("缓存中未找到无人车对象或类型不匹配,跳过任务上下文更新: vehicleId={}", vehicleId); + } } // 用于存储通用车辆状态数据的缓存 @@ -614,8 +574,6 @@ public class DataCollectorService { private String dataSource; } - // 移除周期性处理方法 - 现在由DataProcessingService处理 - /** * 统计当前数据采集服务的健康状态和关键指标 * @return 统计信息字符串 @@ -674,13 +632,9 @@ public class DataCollectorService { notification.getSeat(), notification.getEventDateTime()); - // 缓存航班通知供DataProcessingService推送给前端 + // 缓存航班通知供DataProcessingService处理 cacheFlightNotification(notification); - // 🚀 新增:事件驱动的路由查询 - // 基于航班通知触发路由查询和更新 - triggerRouteQueryByFlightNotification(dto); - } else { log.warn("⚠️ 航班进出港通知数据无效,跳过处理: {}", dto); } @@ -757,211 +711,6 @@ public class DataCollectorService { return flightNotificationCache; } - /** - * 检查指定航班事件的路由是否已经获取过 - * - * @param flightNo 航班号 - * @param type 路由类型(IN/OUT) - * @param time 事件时间戳 - * @return true表示已获取过,false表示未获取过 - */ - private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) { - String cacheKey = flightNo + ":" + type + ":" + time; - return routeRetrievalCache.containsKey(cacheKey); - } - - /** - * 标记指定航班事件的路由已获取 - * - * @param flightNo 航班号 - * @param type 路由类型(IN/OUT) - * @param time 事件时间戳 - */ - private void markRouteAsRetrieved(String flightNo, String type, Long time) { - String cacheKey = flightNo + ":" + type + ":" + time; - routeRetrievalCache.put(cacheKey, System.currentTimeMillis()); - log.debug("标记路由已获取: cacheKey={}, 当前缓存数量={}", cacheKey, routeRetrievalCache.size()); - } - - /** - * 定时清理过期的路由获取缓存记录 - */ - @Scheduled(fixedRate = 3600000) // 每小时执行一次 - public void cleanExpiredRouteCache() { - if (routeRetrievalCache.isEmpty()) { - return; - } - - long expiryTime = System.currentTimeMillis() - (routeCacheExpiryHours * 3600 * 1000L); - int sizeBefore = routeRetrievalCache.size(); - - routeRetrievalCache.entrySet().removeIf(entry -> entry.getValue() < expiryTime); - - int sizeAfter = routeRetrievalCache.size(); - if (sizeBefore != sizeAfter) { - log.info("清理过期路由缓存完成:清理前{}条,清理后{}条,清理了{}条记录", - sizeBefore, sizeAfter, sizeBefore - sizeAfter); - } - } - - /** - * 基于航班通知触发路由查询 - * - * 实现事件驱动的路由更新流程: - * 1. 收到航班进出港通知 - * 2. 使用航班号和类型查询路由参数 - * 3. 调用路由查询接口获取完整路由数据 - * 4. 发布WebSocket路由更新事件 - * - * @param flightNotification 航班进出港通知 - */ - private void triggerRouteQueryByFlightNotification(FlightNotificationDTO flightNotification) { - try { - String flightNo = flightNotification.getFlightNo(); - String routeType = flightNotification.getType(); - - // 检查是否已获取过该航班事件的路由 - if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getTime())) { - log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}", - flightNo, routeType, flightNotification.getTime()); - return; - } - - log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getTime()); - - // 步骤1: 查询航班路由参数 - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams = - dataCollectorDao.getAircraftRouteParams(flightNo, routeType); - - if (routeParams == null || !routeParams.isValid()) { - log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType); - return; - } - - log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}", - routeParams.getFlightNo(), - routeParams.getInRunway(), - routeParams.getOutRunway(), - routeParams.getContactCross(), - routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat()); - - // 步骤2: 基于路由参数调用相应的路由查询接口 - AircraftRouteDTO routeData = null; - - if (routeParams.isArrivalRoute()) { - // 进港路由查询 - routeData = dataCollectorDao.getArrivalRoute( - routeParams.getInRunway(), - routeParams.getOutRunway(), - routeParams.getContactCross(), - routeParams.getSeat() - ); - log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}", - routeParams.getInRunway(), routeParams.getOutRunway(), - routeParams.getContactCross(), routeParams.getSeat()); - } else if (routeParams.isDepartureRoute()) { - // 出港路由查询 - routeData = dataCollectorDao.getDepartureRoute( - routeParams.getInRunway(), - routeParams.getOutRunway(), - routeParams.getStartSeat() - ); - log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}", - routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat()); - } - - // 步骤3: 处理路由查询结果 - if (routeData != null) { - log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}", - routeData.getType(), routeData.getCodes(), routeData.getStatus()); - - // 转换DTO为航空器路由对象 - AircraftRoute aircraftRoute = convertToAircraftRoute(routeData); - - if (aircraftRoute != null) { - // 保存路由到数据库 - saveAircraftRouteToDatabase(flightNo, aircraftRoute); - - // 更新缓存中的航空器路由信息 - updateAircraftRouteInCacheFromNotification(flightNo, aircraftRoute, routeParams); - - // 发布WebSocket路由更新事件 - publishAircraftRouteUpdateEventFromNotification(flightNo, aircraftRoute, routeParams); - - // 标记该航班事件的路由已获取 - markRouteAsRetrieved(flightNo, routeType, flightNotification.getTime()); - - log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getTime()); - } else { - log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo); - } - } else { - log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType); - } - - } catch (Exception e) { - log.error("❌ 航班通知触发路由查询异常: flightNo={}", - flightNotification.getFlightNo(), e); - } - } - - /** - * 基于航班通知更新缓存中的航空器路由信息 - */ - private void updateAircraftRouteInCacheFromNotification(String flightNo, AircraftRoute route, - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) { - MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo); - - if (cachedAircraft != null && cachedAircraft instanceof Aircraft) { - Aircraft aircraft = (Aircraft) cachedAircraft; - - // 根据路由类型更新路由 - if (routeParams.isArrivalRoute()) { - aircraft.setArrivalRoute(route); - aircraft.activateArrivalRoute(); - } else if (routeParams.isDepartureRoute()) { - aircraft.setDepartureRoute(route); - aircraft.activateDepartureRoute(); - } - - // 更新缓存 - activeMovingObjectsCache.put(flightNo, aircraft); - - log.debug("✅ 成功更新航空器路由缓存 (事件驱动): flightNo={}, type={}, codes={}", - flightNo, route.getType(), route.getCodes()); - } else { - log.debug("🔍 缓存中暂无航空器对象,跳过缓存更新 (事件驱动): flightNo={}", flightNo); - } - } - - /** - * 基于航班通知发布航空器路由更新事件 - */ - private void publishAircraftRouteUpdateEventFromNotification(String flightNo, AircraftRoute route, - com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) { - try { - // 创建路由更新事件 - AircraftRouteUpdateEvent routeUpdateEvent = AircraftRouteUpdateEvent.builder() - .flightNo(flightNo) - .routeType(route.getType()) - .routeStatus(route.getStatus()) - .routeCodes(route.getCodes()) - .aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型 - .routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null) - .timestamp(System.currentTimeMillis()) - .eventSource("FLIGHT_NOTIFICATION") // 标识事件来源 - .build(); - - // 发布WebSocket事件 - eventPublisher.publishEvent(routeUpdateEvent); - - log.info("📡 发布航空器路由更新事件 (事件驱动): 航班号={}, 路由类型={}, 事件来源=航班通知", - flightNo, route.getType()); - - } catch (Exception e) { - log.error("❌ 发布航空器路由更新事件失败 (事件驱动): flightNo={}", flightNo, e); - } - } @PreDestroy public void shutdown() { diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/RoutePersistenceService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/RoutePersistenceService.java index 84cd8494..7eb6c3b0 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/RoutePersistenceService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/RoutePersistenceService.java @@ -4,7 +4,6 @@ import com.qaup.collision.common.model.AircraftRoute; import com.qaup.collision.pathconflict.model.entity.TransportRoute; import com.qaup.collision.pathconflict.repository.TransportRouteRepository; import lombok.extern.slf4j.Slf4j; -import org.locationtech.jts.geom.LineString; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -234,11 +233,4 @@ public class RoutePersistenceService { objectName, routeType, routeId, e); } } - - /** - * 验证LineString的有效性 - */ - private boolean isValidLineString(LineString lineString) { - return lineString != null && lineString.isValid() && lineString.getNumPoints() >= 2; - } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/UnmannedVehicleControlService.java b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/UnmannedVehicleControlService.java index f7709161..60bfadc3 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/UnmannedVehicleControlService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/datacollector/service/UnmannedVehicleControlService.java @@ -3,7 +3,6 @@ package com.qaup.collision.datacollector.service; import com.qaup.collision.common.model.spatial.VehicleLocation; import com.qaup.collision.datacollector.model.converter.VehicleCommandConverter; import com.qaup.collision.datacollector.model.dto.VehicleCommand; -import com.qaup.collision.datacollector.model.dto.VehicleStateInfo; import com.qaup.collision.datacollector.model.entity.VehicleCommandEntity; import com.qaup.collision.datacollector.repository.VehicleCommandRepository; import com.qaup.collision.common.model.repository.VehicleLocationRepository; @@ -22,7 +21,6 @@ import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; -import java.util.UUID; /** * 无人车控制服务类 @@ -59,11 +57,7 @@ public class UnmannedVehicleControlService { @Value("${data.collector.vehicle-api.endpoints.vehicle-command:#{null}}") private String vehicleCommandEndpoint; - @Value("${data.collector.vehicle-api.endpoints.vehicle-location}") - private String vehicleLocationEndpoint; - @Value("${data.collector.vehicle-api.endpoints.vehicle-state:#{null}}") - private String vehicleStateEndpoint; /** * 处理无人车控制指令 @@ -155,29 +149,90 @@ public class UnmannedVehicleControlService { } /** - * 获取无人车状态信息 - * + * 获取完整的无人车状态信息 (符合universal_autonomous_vehicle_api规范) + * * @param vehicleId 车辆ID - * @return 车辆状态信息 + * @return 完整的无人车状态数据 */ - public VehicleStateInfo getVehicleState(String vehicleId) { - logger.info("查询无人车状态信息: vehicleId={}", vehicleId); + @Transactional(readOnly = true) + public com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO getCompleteVehicleStatus(String vehicleId) { + logger.info("查询无人车完整状态信息: vehicleId={}", vehicleId); try { - // 如果配置了外部状态查询API,则调用外部系统 - if (vehicleStateEndpoint != null && !vehicleStateEndpoint.trim().isEmpty()) { - return queryVehicleStateFromExternalSystem(vehicleId); + // 直接调用DataCollectorDao的通用状态API + com.qaup.collision.datacollector.dao.DataCollectorDao dataCollectorDao = + applicationContext.getBean(com.qaup.collision.datacollector.dao.DataCollectorDao.class); + + com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData = + dataCollectorDao.getUniversalVehicleStatus(vehicleId); + + if (statusData != null) { + logger.info("成功获取无人车完整状态: vehicleId={}, 电池电量={}%", + vehicleId, + statusData.getBatteryStatus() != null && statusData.getBatteryStatus().getMainBattery() != null + ? statusData.getBatteryStatus().getMainBattery().getChargeLevel() : "未知"); + return statusData; } else { - // 否则基于本地数据构造状态信息 - return buildVehicleStateFromLocalData(vehicleId); + logger.warn("未能获取无人车完整状态: vehicleId={}", vehicleId); + return null; } } catch (Exception e) { - logger.error("查询无人车状态信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e); - throw new RuntimeException("状态信息查询失败: " + e.getMessage(), e); + logger.error("查询无人车完整状态信息失败: vehicleId={}, error={}", vehicleId, e.getMessage(), e); + throw new RuntimeException("完整状态信息查询失败: " + e.getMessage(), e); } } + /** + * 批量获取无人车完整状态信息 + * + * @param vehicleIds 车辆ID列表 + * @return 车辆状态映射表 (vehicleId -> 状态数据) + */ + @Transactional(readOnly = true) + public java.util.Map getBatchVehicleStatus(List vehicleIds) { + logger.info("批量查询无人车完整状态信息: vehicleIds={}", vehicleIds); + + java.util.Map statusMap = new java.util.HashMap<>(); + + if (vehicleIds == null || vehicleIds.isEmpty()) { + logger.warn("车辆ID列表为空"); + return statusMap; + } + + try { + com.qaup.collision.datacollector.dao.DataCollectorDao dataCollectorDao = + applicationContext.getBean(com.qaup.collision.datacollector.dao.DataCollectorDao.class); + + int successCount = 0; + for (String vehicleId : vehicleIds) { + try { + com.qaup.collision.datacollector.model.dto.UniversalVehicleStatusDTO statusData = + dataCollectorDao.getUniversalVehicleStatus(vehicleId); + + if (statusData != null) { + statusMap.put(vehicleId, statusData); + successCount++; + } + } catch (Exception e) { + logger.warn("获取单个车辆状态失败: vehicleId={}, error={}", vehicleId, e.getMessage()); + } + } + + logger.info("批量查询无人车状态完成: 总数={}, 成功={}, 失败={}", + vehicleIds.size(), successCount, vehicleIds.size() - successCount); + + return statusMap; + + } catch (Exception e) { + logger.error("批量查询无人车完整状态信息失败: vehicleIds={}, error={}", vehicleIds, e.getMessage(), e); + throw new RuntimeException("批量状态信息查询失败: " + e.getMessage(), e); + } + } + + @Autowired + private org.springframework.context.ApplicationContext applicationContext; + /** * 验证控制指令参数 */ @@ -238,107 +293,5 @@ public class UnmannedVehicleControlService { } } - /** - * 从外部系统查询车辆状态 - */ - private VehicleStateInfo queryVehicleStateFromExternalSystem(String vehicleId) { - try { - String url = vehicleApiBaseUrl + vehicleStateEndpoint; - - HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/json"); - - // 构造查询请求参数 - VehicleStateQueryRequest queryRequest = new VehicleStateQueryRequest(); - queryRequest.setTransId(UUID.randomUUID().toString()); - queryRequest.setTimestamp(System.currentTimeMillis()); - queryRequest.setVehicleId(vehicleId); - queryRequest.setSingle(true); - - HttpEntity requestEntity = new HttpEntity<>(queryRequest, headers); - - ResponseEntity response = restTemplate.exchange( - url, HttpMethod.POST, requestEntity, VehicleStateInfo[].class); - - - if (response != null && response.getStatusCode().is2xxSuccessful()) { - VehicleStateInfo[] body = response.getBody(); - if (body != null && body.length > 0) { - VehicleStateInfo stateInfo = body[0]; - logger.info("从外部系统获取车辆状态成功: vehicleId={}", vehicleId); - return stateInfo; - } else { - logger.warn("外部系统未返回车辆状态: vehicleId={}", vehicleId); - return null; - } - } else { - logger.warn("外部系统调用失败: vehicleId={}, status={}", vehicleId, response != null ? response.getStatusCode() : "无响应"); - return null; - } - } catch (Exception e) { - logger.error("从外部系统查询车辆状态异常: vehicleId={}, error={}", vehicleId, e.getMessage(), e); - return null; - } - } - - /** - * 基于本地数据构造车辆状态信息 - */ - private VehicleStateInfo buildVehicleStateFromLocalData(String vehicleId) { - // 查询最近的位置信息判断车辆是否在线 - try { - Long vehicleIdLong = Long.parseLong(vehicleId); - Optional recentLocation = vehicleLocationRepository.findLatestByVehicleId(vehicleIdLong); - - boolean isOnline = recentLocation.isPresent(); - - // 构造基本状态信息 - VehicleStateInfo stateInfo = new VehicleStateInfo(); - stateInfo.setTransId(UUID.randomUUID().toString()); - stateInfo.setTimestamp(System.currentTimeMillis()); - stateInfo.setVehicleId(vehicleIdLong); // 使用Long类型 - stateInfo.setLoginStatus(isOnline); - stateInfo.setFaultInfo(List.of()); // 空故障列表 - stateInfo.setActiveSafety(false); - stateInfo.setRc(false); - stateInfo.setCommand(0); // 0表示恢复状态 - stateInfo.setAirportInfo(List.of()); - stateInfo.setVehicleMode(isOnline ? 2 : 5); // 2:自动, 5:故障等待 - stateInfo.setGearState(2); // 2:D档 - stateInfo.setChassisReady(isOnline); - stateInfo.setCollisionStatus(false); - stateInfo.setClearance(0); - stateInfo.setTurnSignalStatus(0); - stateInfo.setPointCloud(List.of()); - - logger.info("基于本地数据构造车辆状态: vehicleId={}, isOnline={}", vehicleId, isOnline); - return stateInfo; - } catch (NumberFormatException e) { - logger.warn("无效的vehicleId格式: {}", vehicleId); - return null; - } - } - - /** - * 车辆状态查询请求内部类 - */ - private static class VehicleStateQueryRequest { - private String transId; - private long timestamp; - private String vehicleId; - private boolean single; - - public String getTransId() { return transId; } - public void setTransId(String transId) { this.transId = transId; } - - public long getTimestamp() { return timestamp; } - public void setTimestamp(long timestamp) { this.timestamp = timestamp; } - - public String getVehicleId() { return vehicleId; } - public void setVehicleId(String vehicleId) { this.vehicleId = vehicleId; } - - public boolean isSingle() { return single; } - public void setSingle(boolean single) { this.single = single; } - } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java index d77ef2ee..97be3ca0 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java +++ b/qaup-collision/src/main/java/com/qaup/collision/dataprocessing/service/DataProcessingService.java @@ -32,8 +32,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import com.qaup.collision.datacollector.dao.DataCollectorDao; +import com.qaup.collision.datacollector.dto.AircraftRouteDTO; +import com.qaup.collision.common.model.AircraftRoute; + import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.PrecisionModel; @@ -503,6 +508,9 @@ public class DataProcessingService { log.info("📡 发布航班进出港通知WebSocket事件: 航班号={}, 事件类型={}, 通知级别={}", event.getFlightNo(), event.getEventType(), event.getNotificationLevel()); + + // 触发基于航班通知的路由查询和更新处理 + triggerRouteQueryByFlightNotification(notification); } else { log.warn("⚠️ 航班进出港通知WebSocket事件创建失败或无效: {}", notification.getFlightNo()); } @@ -977,4 +985,287 @@ public class DataProcessingService { log.error("🚦 发布红绿灯状态事件失败: intersectionId={}", payload.getIntersectionId(), e); } } + + // ==================== 航班路由处理相关方法 ==================== + + // 路由获取状态缓存:Key=flightNo:type:time, Value=获取时间戳 + private final Map routeRetrievalCache = new ConcurrentHashMap<>(); + + /** + * 基于航班通知触发路由查询 + * + * 实现事件驱动的路由更新流程: + * 1. 收到航班进出港通知 + * 2. 使用航班号和类型查询路由参数 + * 3. 调用路由查询接口获取完整路由数据 + * 4. 发布WebSocket路由更新事件 + * + * @param flightNotification 航班进出港通知 + */ + private void triggerRouteQueryByFlightNotification(FlightNotification flightNotification) { + try { + String flightNo = flightNotification.getFlightNo(); + String routeType = flightNotification.getType().name(); + + // 检查是否已获取过该航班事件的路由 + if (isRouteAlreadyRetrieved(flightNo, routeType, flightNotification.getEventTime())) { + log.info("🔄 航班路由已获取过,跳过重复查询: 航班号={}, 类型={}, 时间={}", + flightNo, routeType, flightNotification.getEventTime()); + return; + } + + log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime()); + + // 获取DataCollectorDao进行路由数据查询 + DataCollectorDao dataCollectorDao = applicationContext.getBean(DataCollectorDao.class); + + // 步骤1: 查询航班路由参数 + com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams = + dataCollectorDao.getAircraftRouteParams(flightNo, routeType); + + if (routeParams == null || !routeParams.isValid()) { + log.warn("⚠️ 未能获取有效的航班路由参数: flightNo={}, routeType={}", flightNo, routeType); + return; + } + + log.info("✅ 成功获取航班路由参数: flightNo={}, inRunway={}, outRunway={}, contactCross={}, seat/startSeat={}", + routeParams.getFlightNo(), + routeParams.getInRunway(), + routeParams.getOutRunway(), + routeParams.getContactCross(), + routeParams.isArrivalRoute() ? routeParams.getSeat() : routeParams.getStartSeat()); + + // 步骤2: 基于路由参数调用相应的路由查询接口 + AircraftRouteDTO routeData = null; + + if (routeParams.isArrivalRoute()) { + // 进港路由查询 + routeData = dataCollectorDao.getArrivalRoute( + routeParams.getInRunway(), + routeParams.getOutRunway(), + routeParams.getContactCross(), + routeParams.getSeat() + ); + log.info("🛬 查询进港路由: inRunway={}, outRunway={}, contactCross={}, seat={}", + routeParams.getInRunway(), routeParams.getOutRunway(), + routeParams.getContactCross(), routeParams.getSeat()); + } else if (routeParams.isDepartureRoute()) { + // 出港路由查询 + routeData = dataCollectorDao.getDepartureRoute( + routeParams.getInRunway(), + routeParams.getOutRunway(), + routeParams.getStartSeat() + ); + log.info("🛫 查询出港路由: inRunway={}, outRunway={}, startSeat={}", + routeParams.getInRunway(), routeParams.getOutRunway(), routeParams.getStartSeat()); + } + + // 步骤3: 处理路由查询结果 + if (routeData != null) { + log.info("🎯 成功获取{}路由数据: 编码={}, 状态={}", + routeData.getType(), routeData.getCodes(), routeData.getStatus()); + + // 转换DTO为航空器路由对象 + AircraftRoute aircraftRoute = convertToAircraftRoute(routeData); + + if (aircraftRoute != null) { + // 保存路由到数据库 + saveAircraftRouteToDatabase(flightNo, aircraftRoute); + + // 更新缓存中的航空器路由信息 + updateAircraftRouteInCache(flightNo, aircraftRoute, routeParams); + + // 发布WebSocket路由更新事件 + publishAircraftRouteUpdateEvent(flightNo, aircraftRoute, routeParams); + + // 标记该航班事件的路由已获取 + markRouteAsRetrieved(flightNo, routeType, flightNotification.getEventTime()); + + log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}, 时间={}", flightNo, routeType, flightNotification.getEventTime()); + } else { + log.warn("⚠️ 路由数据转换失败: flightNo={}", flightNo); + } + } else { + log.warn("⚠️ 未获取到路由数据: flightNo={}, routeType={}", flightNo, routeType); + } + + } catch (Exception e) { + log.error("❌ 航班通知触发路由查询异常: flightNo={}", + flightNotification.getFlightNo(), e); + } + } + + /** + * 检查指定航班事件的路由是否已经获取过 + */ + private boolean isRouteAlreadyRetrieved(String flightNo, String type, Long time) { + String cacheKey = flightNo + ":" + type + ":" + time; + return routeRetrievalCache.containsKey(cacheKey); + } + + /** + * 标记指定航班事件的路由已获取 + */ + private void markRouteAsRetrieved(String flightNo, String type, Long time) { + String cacheKey = flightNo + ":" + type + ":" + time; + routeRetrievalCache.put(cacheKey, System.currentTimeMillis()); + log.debug("标记路由已获取: cacheKey={}, 当前缓存数量={}", cacheKey, routeRetrievalCache.size()); + } + + /** + * 将AircraftRouteDTO转换为AircraftRoute对象 + * 使用JTS将多个LineString段合并为单一连续路径 + */ + private AircraftRoute convertToAircraftRoute(AircraftRouteDTO routeDTO) { + if (routeDTO == null || routeDTO.getGeoPath() == null) { + return null; + } + + try { + // 获取路由几何处理器 + com.qaup.collision.datacollector.util.RouteGeometryProcessor routeGeometryProcessor = + applicationContext.getBean(com.qaup.collision.datacollector.util.RouteGeometryProcessor.class); + + // 创建路由段列表 + List routeSegments = new ArrayList<>(); + List lineStringSegments = new ArrayList<>(); + + if (routeDTO.getGeoPath().getFeatures() != null) { + for (AircraftRouteDTO.Feature feature : routeDTO.getGeoPath().getFeatures()) { + if (feature.getGeometry() != null && feature.getGeometry().getCoordinates() != null) { + // 转换坐标格式:从List>到List + List points = feature.getGeometry().getCoordinates().stream() + .map(coord -> geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(coord.get(0), coord.get(1)))) + .collect(java.util.stream.Collectors.toList()); + + // 创建路由段 + AircraftRoute.RouteSegment segment = AircraftRoute.RouteSegment.builder() + .code(feature.getProperties() != null ? feature.getProperties().getCode() : "") + .coordinates(points) + .build(); + routeSegments.add(segment); + + // 创建LineString段用于合并 + if (points.size() >= 2) { + org.locationtech.jts.geom.Coordinate[] coords = points.stream() + .map(point -> point.getCoordinate()) + .toArray(org.locationtech.jts.geom.Coordinate[]::new); + org.locationtech.jts.geom.LineString lineString = geometryFactory.createLineString(coords); + lineStringSegments.add(lineString); + } + } + } + } + + // 使用JTS将多个LineString段合并为单一连续路径 + org.locationtech.jts.geom.LineString mergedGeometry = null; + if (!lineStringSegments.isEmpty()) { + mergedGeometry = routeGeometryProcessor.mergeLineStrings(lineStringSegments); + + if (mergedGeometry != null && routeGeometryProcessor.isValidLineString(mergedGeometry)) { + // 可选:简化路径以减少冗余点(容差:1米) + mergedGeometry = routeGeometryProcessor.simplifyLineString(mergedGeometry, 1.0); + log.info("成功将 {} 个路由段合并为单一路径,总长度: {} 个坐标点", + lineStringSegments.size(), mergedGeometry.getNumPoints()); + } else { + log.warn("路由段合并失败或结果无效"); + mergedGeometry = null; + } + } + + return AircraftRoute.builder() + .type(routeDTO.getType()) + .status(routeDTO.getStatus()) + .codes(routeDTO.getCodes()) + .geometry(mergedGeometry) // 使用合并后的单一LineString + .routeSegments(routeSegments) + .build(); + + } catch (Exception e) { + log.error("转换航空器路由数据失败", e); + return null; + } + } + + /** + * 保存航空器路由到数据库(独立于缓存) + */ + private void saveAircraftRouteToDatabase(String flightNo, AircraftRoute route) { + try { + log.info("开始保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType()); + + // 获取路由持久化服务 + com.qaup.collision.datacollector.service.RoutePersistenceService routePersistenceService = + applicationContext.getBean(com.qaup.collision.datacollector.service.RoutePersistenceService.class); + + boolean saveSuccess = routePersistenceService.saveAircraftRoute(flightNo, route); + + if (saveSuccess) { + log.info("成功保存航空器路由到数据库: flightNo={}, type={}", flightNo, route.getType()); + } else { + log.warn("保存航空器路由到数据库失败: flightNo={}, type={}", flightNo, route.getType()); + } + } catch (Exception e) { + log.error("保存航空器路由到数据库异常: flightNo={}, type={}", flightNo, route.getType(), e); + } + } + + /** + * 更新缓存中的航空器路由信息 + */ + private void updateAircraftRouteInCache(String flightNo, AircraftRoute route, + com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) { + MovingObject cachedAircraft = activeMovingObjectsCache.get(flightNo); + + if (cachedAircraft != null && cachedAircraft instanceof com.qaup.collision.common.model.Aircraft) { + com.qaup.collision.common.model.Aircraft aircraft = (com.qaup.collision.common.model.Aircraft) cachedAircraft; + + // 根据路由类型更新路由 + if (routeParams.isArrivalRoute()) { + aircraft.setArrivalRoute(route); + aircraft.activateArrivalRoute(); + } else if (routeParams.isDepartureRoute()) { + aircraft.setDepartureRoute(route); + aircraft.activateDepartureRoute(); + } + + // 更新缓存 + activeMovingObjectsCache.put(flightNo, aircraft); + + log.debug("✅ 成功更新航空器路由缓存: flightNo={}, type={}, codes={}", + flightNo, route.getType(), route.getCodes()); + } else { + log.debug("🔍 缓存中暂无航空器对象,跳过缓存更新: flightNo={}", flightNo); + } + } + + /** + * 发布航空器路由更新事件 + */ + private void publishAircraftRouteUpdateEvent(String flightNo, AircraftRoute route, + com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO routeParams) { + try { + // 创建路由更新事件 + com.qaup.collision.websocket.event.AircraftRouteUpdateEvent routeUpdateEvent = + com.qaup.collision.websocket.event.AircraftRouteUpdateEvent.builder() + .flightNo(flightNo) + .routeType(route.getType()) + .routeStatus(route.getStatus()) + .routeCodes(route.getCodes()) + .aircraftStatus(routeParams.getRouteType()) // 使用路由参数中的类型 + .routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null) + .timestamp(System.currentTimeMillis()) + .eventSource("FLIGHT_NOTIFICATION") // 标识事件来源 + .build(); + + // 发布WebSocket事件 + eventPublisher.publishEvent(routeUpdateEvent); + + log.info("📡 发布航空器路由更新事件: 航班号={}, 路由类型={}, 事件来源=航班通知", + flightNo, route.getType()); + + } catch (Exception e) { + log.error("❌ 发布航空器路由更新事件失败: flightNo={}", flightNo, e); + } + } } \ No newline at end of file diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/LocationRuleQueryServiceImpl.java b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/LocationRuleQueryServiceImpl.java index d7a0641a..ac0c8ee3 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/LocationRuleQueryServiceImpl.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/LocationRuleQueryServiceImpl.java @@ -427,17 +427,6 @@ public class LocationRuleQueryServiceImpl implements LocationRuleQueryService { return path; } - /** - * 检查复杂时间模式匹配 - * @param rule 规则 - * @param timestamp 时间戳 - * @return 如果匹配返回true - */ - private boolean matchesComplexTimePatterns(SpatialRule rule, LocalDateTime timestamp) { - // 使用TimeWindowMatchingService处理复杂时间模式匹配 - return timeWindowMatchingService.matchesComplexTimePatterns(rule.getTimePatterns(), timestamp); - } - /** * 从区域检查获取几何形状 * @param areaId 区域ID diff --git a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java index 9f8710ba..b7d9772e 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java +++ b/qaup-collision/src/main/java/com/qaup/collision/rule/service/impl/SpatialRuleServiceImpl.java @@ -1083,26 +1083,6 @@ public class SpatialRuleServiceImpl implements SpatialRuleService { return true; } - /** - * 检查星期名称匹配 "MON,WED,FRI" - */ - private boolean matchesWeekdayNames(String weekdays, LocalDateTime timestamp) { - try { - String[] dayNames = weekdays.split(","); - String currentDay = timestamp.getDayOfWeek().name().substring(0, 3); // MON, TUE, etc. - - for (String dayName : dayNames) { - if (dayName.trim().equalsIgnoreCase(currentDay)) { - return true; - } - } - return false; - } catch (Exception e) { - logger.warn("检查星期名称失败: {}", weekdays, e); - return true; - } - } - private JsonNode parseJsonString(String jsonString) { try { if (jsonString == null || jsonString.trim().isEmpty()) { diff --git a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java index 1d519c5e..76a35b95 100644 --- a/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java +++ b/qaup-collision/src/main/java/com/qaup/collision/websocket/broadcaster/WebSocketMessageBroadcaster.java @@ -8,7 +8,6 @@ import com.qaup.collision.websocket.event.RuleStateChangeWebSocketEvent; import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent; import com.qaup.collision.websocket.event.GeofenceAlertWebSocketEvent; import com.qaup.collision.websocket.event.PathConflictAlertWebSocketEvent; -import com.qaup.collision.websocket.event.TrafficLightStatusEvent; import com.qaup.collision.websocket.event.VehicleCommandEvent; import com.qaup.collision.websocket.event.VehicleStatusUpdateEvent; import com.qaup.collision.websocket.message.CollisionWarningPayload; @@ -18,7 +17,6 @@ import com.qaup.collision.websocket.message.PositionUpdatePayload; import com.qaup.collision.websocket.message.RuleExecutionStatusPayload; import com.qaup.collision.websocket.message.RuleStateChangePayload; import com.qaup.collision.websocket.message.RuleViolationPayload; -import com.qaup.collision.websocket.message.TrafficLightStatusPayload; import com.qaup.collision.websocket.message.VehicleCommandPayload; import com.qaup.collision.websocket.message.VehicleStatusUpdatePayload; import com.qaup.collision.websocket.message.UniversalMessage; @@ -119,21 +117,6 @@ public class WebSocketMessageBroadcaster { broadcastMessageInternal(message); } - /** - * 处理红绿灯状态事件 - * @param event 红绿灯状态事件 - */ - @EventListener - public void handleTrafficLightStatus(TrafficLightStatusEvent event) { - UniversalMessage message = UniversalMessage.builder() - .type(MessageTypeConstants.TRAFFIC_LIGHT_STATUS) - .timestamp(event.getTimestamp()) - .messageId(generateMessageId()) - .payload((TrafficLightStatusPayload) event.getPayload()) - .build(); - - broadcastMessageInternal(message); - } /** * 处理碰撞预警事件 diff --git a/tools/__pycache__/mock_unmanned_vehicle.cpython-313.pyc b/tools/__pycache__/mock_unmanned_vehicle.cpython-313.pyc new file mode 100644 index 00000000..41572c39 Binary files /dev/null and b/tools/__pycache__/mock_unmanned_vehicle.cpython-313.pyc differ diff --git a/tools/mock_server-copy_20250919.py b/tools/mock_server-copy_20250919.py deleted file mode 100644 index 2a245fc8..00000000 --- a/tools/mock_server-copy_20250919.py +++ /dev/null @@ -1,2722 +0,0 @@ -from flask import Flask, jsonify, request -import time -import math -import logging -import os -import uuid -from typing import Any, Literal, final, TypedDict - -# 创建 logs 目录(如果不存在) -if not os.path.exists('logs'): - os.makedirs('logs') - -# 配置日志 -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('logs/mock_server.log'), - logging.StreamHandler() - ] -) - -app = Flask(__name__) - -# 为位置与移动对象定义类型,避免使用 Any -class Point(TypedDict): - latitude: float - longitude: float - -# 必填字段 -class MovingObjectRequired(TypedDict): - latitude: float - longitude: float - time: int - speed: float - moving_to_end: bool - start_point: Point - end_point: Point - -# 可选字段(total=False) -class MovingObject(MovingObjectRequired, total=False): - flightNo: str - vehicleNo: str - wait_until: float - direction: float - - - -# 地球半径(米) -EARTH_RADIUS = 6378137.0 - -COMMAND_PRIORITIES = { - "ALERT": 5, - "WARNING": 3, - "PARKING": 2, - "RESUME": 1 -} - -def meters_to_degrees(meters: float, latitude: float) -> tuple[float, float]: - """ - 将米转换为度,考虑纬度的影响 - """ - # 纬度方向:1度 = 111,319.9米 - meters_per_deg_lat: float = 111319.9 - # 经度方向:1度 = 111,319.9 * cos(latitude)米 - meters_per_deg_lon: float = meters_per_deg_lat * math.cos(math.radians(latitude)) - return meters / meters_per_deg_lat, meters / meters_per_deg_lon - -# 距离配置(米) -DIST_300M = 300 -DIST_200M = 200 -DIST_150M = 150 -DIST_100M = 100 -DIST_50M = 50 - -# 时间配置(秒) -WAIT_TIME_AFTER_RETURN = 0.1 # 返回起点后的等待时间(进一步优化:减少到0.1秒) -UPDATE_INTERVAL = 1.0 # 位置更新间隔, 默认 1 秒 -TRAFFIC_LIGHT_SWITCH_INTERVAL = 10.0 # 红绿灯切换间隔 - -# 根据 route.md 定义的坐标点 - -# 飞机 CA1234 路径 -CA_START = {"longitude": 120.086263, "latitude": 36.370484} # 飞机起点 -CA_END = {"longitude": 120.080996, "latitude": 36.369105} # 飞机终点 - -# 飞机 MU5123 路径 -MU_START = {"longitude": 120.088076, "latitude": 36.374179} # 飞机起点 -MU_END = {"longitude": 120.077971, "latitude": 36.371503} # 飞机终点 - -# 特勤车 鲁B123 路径 -SPECIAL_VEHICLE_START = {"longitude": 120.080801, "latitude": 36.366626} # 特勤车起点 -SPECIAL_VEHICLE_END = {"longitude": 120.083899, "latitude": 36.367403} # 特勤车终点 - -# 普通车 鲁B234 路径 -NORMAL_VEHICLE_START = {"longitude": 120.087259, "latitude": 36.368299} # 普通车起点 -NORMAL_VEHICLE_END = {"longitude": 120.083899, "latitude": 36.367403} # 普通车终点 - -# 无人车A 鲁B567 路径 -UNMANNED_A_START = {"longitude": 120.083084, "latitude": 36.369696} # 无人车A起点 -UNMANNED_A_END = {"longitude": 120.084637, "latitude": 36.365617} # 无人车A终点 - -# 无人车B 鲁B579 路径 -UNMANNED_B_START = {"longitude": 120.086965, "latitude": 36.368599} # 无人车B起点 -UNMANNED_B_END = {"longitude": 120.086263, "latitude": 36.370484} # 无人车B终点 - -# 飞机和车辆尺寸(半径 米) -AIRCRAFT_SIZE_M = 30.0 -VEHICLE_SIZE_M = 10.0 - -# 航班数据配置 - 用于进出港查询接口(简化版) -current_time = time.time() -flight_data = { - "CA8901": { # 出港航班 - "flightNo": "CA8901", - "type": "OUT", - "runway": "35", - "contactCross": "L4", - "seat": "201", - "cycle_start": current_time, - "active_duration": 600, # 10分钟活跃期 - "gap_duration": 10, # 10秒间隔期 - "fixed_time": int(current_time * 1000), # 固定的滑出时间 - "status": "active" - }, - "MU2678": { # 进港航班 - "flightNo": "MU2678", - "type": "IN", - "runway": "17", - "contactCross": "B3", - "seat": "156", - "cycle_start": current_time, - "active_duration": 900, # 15分钟活跃期 - "gap_duration": 10, # 10秒间隔期 - "fixed_time": int(current_time * 1000), # 固定的落地时间 - "status": "active" - } -} - -# CA3456 航空器路由参数配置 -aircraft_route_params = { - "CA3456": { - "arrival": { # 进港参数 - "inRunway": "35", - "outRunway": "34", - "contactCross": "F1", - "seat": "138" - }, - "departure": { # 出港参数 - "inRunway": "35", - "outRunway": "34", - "startSeat": "138" - } - }, - "CA1234": { - "arrival": { - "inRunway": "17", - "outRunway": "35", - "contactCross": "A2", - "seat": "201" - }, - "departure": { - "inRunway": "17", - "outRunway": "35", - "startSeat": "201" - } - }, - "MU5123": { - "arrival": { - "inRunway": "35", - "outRunway": "17", - "contactCross": "B3", - "seat": "156" - }, - "departure": { - "inRunway": "35", - "outRunway": "17", - "startSeat": "156" - } - } -} - -@app.route('/aircraftRouteParamsController/getRouteParams', methods=['GET', 'OPTIONS']) -def get_aircraft_route_params(): - """获取航空器路由查询参数""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - - # 获取航班号和路由类型参数 - flight_no = request.args.get('flightNo') - route_type = request.args.get('routeType', '').upper() # IN 或 OUT - - if not flight_no: - return jsonify({ - "status": 400, - "msg": "缺少flightNo参数", - "data": None - }), 400 - - if route_type not in ['IN', 'OUT']: - return jsonify({ - "status": 400, - "msg": "routeType参数必须是IN或OUT", - "data": None - }), 400 - - # 查找航班路由参数 - aircraft_params = aircraft_route_params.get(flight_no) - if not aircraft_params: - return jsonify({ - "status": 404, - "msg": f"未找到航班 {flight_no} 的路由参数", - "data": None - }), 404 - - # 根据路由类型返回对应参数 - if route_type == 'IN': - route_params = aircraft_params.get('arrival') - if not route_params: - return jsonify({ - "status": 404, - "msg": f"未找到航班 {flight_no} 的进港路由参数", - "data": None - }), 404 - - logging.info(f"进港路由参数查询: flightNo={flight_no}, params={route_params}") - return jsonify({ - "status": 200, - "msg": "进港路由参数查询成功", - "data": { - "flightNo": flight_no, - "routeType": "IN", - "inRunway": route_params["inRunway"], - "outRunway": route_params["outRunway"], - "contactCross": route_params["contactCross"], - "seat": route_params["seat"], - "timestamp": int(time.time() * 1000) - } - }) - - else: # OUT - route_params = aircraft_params.get('departure') - if not route_params: - return jsonify({ - "status": 404, - "msg": f"未找到航班 {flight_no} 的出港路由参数", - "data": None - }), 404 - - logging.info(f"出港路由参数查询: flightNo={flight_no}, params={route_params}") - return jsonify({ - "status": 200, - "msg": "出港路由参数查询成功", - "data": { - "flightNo": flight_no, - "routeType": "OUT", - "inRunway": route_params["inRunway"], - "outRunway": route_params["outRunway"], - "startSeat": route_params["startSeat"], - "timestamp": int(time.time() * 1000) - } - }) - -ca3456_status = { - "flightNo": "CA3456", - "type": "IN", # IN: 进港, ARRIVED: 停留, OUT: 出港 - "inRunway": "35", - "outRunway": "34", - "contactCross": "F1", - "seat": "138", - "timestamp": int(time.time() * 1000), - "status_start_time": time.time(), - "cycle_duration": 77, # 总循环时间: 30+15+30+2=77秒 - "arrival_duration": 30, # 进港阶段持续时间 - "wait_duration": 15, # 停留时间(15秒,方便测试) - "departure_duration": 30 # 出港阶段持续时间 -} - -# 航空器路由数据 - 使用API文档中的完整示例数据 -aircraft_routes = { - "arrival": { - "type": "IN", - "status": "COMPLETE", - "codes": "F1,L4,138", - "geometry": None, - "geoPath": { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050742275893088E7, 4026164.644604296], - [4.050742342874898E7, 4026162.545793306] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050743615407222E7, 4026122.672208275], - [4.050743684026714E7, 4026120.146600441], - [4.050743730372977E7, 4026117.570797326], - [4.050743754093282E7, 4026114.964402468], - [4.050743757419489E7, 4026113.602043673], - [4.050743755007106E7, 4026112.347252104], - [4.050743733107493E7, 4026109.739264329], - [4.050743688561112E7, 4026107.160287504] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050717462298063E7, 4026091.904402129], - [4.050716820216861E7, 4026089.855066455] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050722536188381E7, 4026108.097315812], - [4.050720821283463E7, 4026102.624334418] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050727144214725E7, 4026112.527790001], - [4.050726278505515E7, 4026114.415332655] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050731882638656E7, 4026102.196402456], - [4.050727312768086E7, 4026112.160285922] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050738651815705E7, 4026087.437277401], - [4.050734647450486E7, 4026096.168165339] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050714461981621E7, 4026082.328947974], - [4.05071119278174E7, 4026071.895744022] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050734647450486E7, 4026096.168165339], - [4.050733913391775E7, 4026097.768664928] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050689454491971E7, 4026002.519737061], - [4.050693265139649E7, 4026014.681113256], - [4.050697075787329E7, 4026026.842489458] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050741162298967E7, 4026083.825606086], - [4.050741416963529E7, 4026084.285112275], - [4.050741669524226E7, 4026084.971307588], - [4.050741915143272E7, 4026085.875012957], - [4.050742151951354E7, 4026086.989350639], - [4.050742378146222E7, 4026088.305839852], - [4.050742592006397E7, 4026089.814461317], - [4.050742791904272E7, 4026091.503733515], - [4.050742976318505E7, 4026093.360800063], - [4.050743143845592E7, 4026095.371527565], - [4.050743293210549E7, 4026097.52061317], - [4.050743423276621E7, 4026099.791701039], - [4.050743533053925E7, 4026102.167506821], - [4.05074362170699E7, 4026104.629949201], - [4.050743683431807E7, 4026106.966150228], - [4.050743688561112E7, 4026107.160287504] - ] - }, - "properties": { - "code": "" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050697552118288E7, 4026028.362661481], - [4.050697075787329E7, 4026026.842489458] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050704221346159E7, 4026049.646966941], - [4.050703137036284E7, 4026046.18647901] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050708746004742E7, 4026064.087051627], - [4.050704840096232E7, 4026051.621473066] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05071119278174E7, 4026071.895744022], - [4.050710556055213E7, 4026069.863682419] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050741939071107E7, 4026175.198599438], - [4.05074216811156E7, 4026168.021835575], - [4.050742275893088E7, 4026164.644604296] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050753774654577E7, 4026246.448261945], - [4.050749515081406E7, 4026236.848849251], - [4.050744870329395E7, 4026226.381394062] - ] - }, - "properties": { - "code": "138" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050753774654577E7, 4026246.448261945], - [4.0507613391983E7, 4026263.495786141], - [4.05076192451935E7, 4026264.814870958], - [4.050762119626365E7, 4026265.254565894] - ] - }, - "properties": { - "code": "138" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050742342874898E7, 4026162.545793306], - [4.050743615407222E7, 4026122.672208275] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050716820216861E7, 4026089.855066455], - [4.050714461981621E7, 4026082.328947974] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050720821283463E7, 4026102.624334418], - [4.050717462298063E7, 4026091.904402129] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050726278505515E7, 4026114.415332655], - [4.050725934642086E7, 4026115.009285077], - [4.050725586910526E7, 4026115.301280484], - [4.050725237957282E7, 4026115.289096617], - [4.050724890438099E7, 4026114.9728262], - [4.050724546997807E7, 4026114.354876244], - [4.050724210250195E7, 4026113.43994972], - [4.050722536188381E7, 4026108.097315812] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050727312768086E7, 4026112.160285922], - [4.050727144214725E7, 4026112.527790001] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050733913391775E7, 4026097.768664928], - [4.050731882638656E7, 4026102.196402456] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05074011833275E7, 4026084.239767811], - [4.050738651815705E7, 4026087.437277401] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05074011833275E7, 4026084.239767811], - [4.050740376230329E7, 4026083.794303922], - [4.050740637029002E7, 4026083.5753078], - [4.050740898743934E7, 4026083.584446135], - [4.050741162298967E7, 4026083.825606086] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050744870329395E7, 4026226.381394062], - [4.050744533581797E7, 4026225.466467002], - [4.050744206089737E7, 4026224.261526533], - [4.050743890345625E7, 4026222.775742978], - [4.050743588752466E7, 4026221.020424048], - [4.050743303605565E7, 4026219.00892878], - [4.050743037075064E7, 4026216.756565868], - [4.050742791189419E7, 4026214.280477153], - [4.050742567819968E7, 4026211.599507165], - [4.050742368666689E7, 4026208.734059705], - [4.050742195245258E7, 4026205.705942559], - [4.050742048875517E7, 4026202.538201526], - [4.050741930671428E7, 4026199.254945029], - [4.050741841532595E7, 4026195.88116063], - [4.050741782137419E7, 4026192.442524868], - [4.050741752937933E7, 4026188.965207836], - [4.050741754156362E7, 4026185.475674017], - [4.050741785783435E7, 4026182.000480871], - [4.050741847578449E7, 4026178.566076715], - [4.050741939071107E7, 4026175.198599438] - ] - }, - "properties": { - "code": "" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050684675534101E7, 4025987.268076611], - [4.050685643844293E7, 4025990.358360866], - [4.050689454491971E7, 4026002.519737061] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050703137036284E7, 4026046.18647901], - [4.05070295640735E7, 4026045.610016264], - [4.050701981996215E7, 4026042.500261312], - [4.050697623399237E7, 4026028.590148907], - [4.050697552118288E7, 4026028.362661481] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050704840096232E7, 4026051.621473066], - [4.050704221346159E7, 4026049.646966941] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050710556055213E7, 4026069.863682419], - [4.050708746004742E7, 4026064.087051627] - ] - }, - "properties": { - "code": "F1" - } - } - ] - } - }, - "departure": { - "type": "OUT", - "status": "COMPLETE", - "codes": "138,L4,F1", - "geometry": None, - "geoPath": { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050742275893088E7, 4026164.644604296], - [4.050742342874898E7, 4026162.545793306] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050743615407222E7, 4026122.672208275], - [4.050743684026714E7, 4026120.146600441], - [4.050743730372977E7, 4026117.570797326], - [4.050743754093282E7, 4026114.964402468], - [4.050743757419489E7, 4026113.602043673], - [4.050743755007106E7, 4026112.347252104], - [4.050743733107493E7, 4026109.739264329], - [4.050743688561112E7, 4026107.160287504] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050717462298063E7, 4026091.904402129], - [4.050716820216861E7, 4026089.855066455] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050722536188381E7, 4026108.097315812], - [4.050720821283463E7, 4026102.624334418] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050727144214725E7, 4026112.527790001], - [4.050726278505515E7, 4026114.415332655] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050731882638656E7, 4026102.196402456], - [4.050727312768086E7, 4026112.160285922] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050738651815705E7, 4026087.437277401], - [4.050734647450486E7, 4026096.168165339] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050714461981621E7, 4026082.328947974], - [4.05071119278174E7, 4026071.895744022] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050734647450486E7, 4026096.168165339], - [4.050733913391775E7, 4026097.768664928] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050689454491971E7, 4026002.519737061], - [4.050693265139649E7, 4026014.681113256], - [4.050697075787329E7, 4026026.842489458] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050741162298967E7, 4026083.825606086], - [4.050741416963529E7, 4026084.285112275], - [4.050741669524226E7, 4026084.971307588], - [4.050741915143272E7, 4026085.875012957], - [4.050742151951354E7, 4026086.989350639], - [4.050742378146222E7, 4026088.305839852], - [4.050742592006397E7, 4026089.814461317], - [4.050742791904272E7, 4026091.503733515], - [4.050742976318505E7, 4026093.360800063], - [4.050743143845592E7, 4026095.371527565], - [4.050743293210549E7, 4026097.52061317], - [4.050743423276621E7, 4026099.791701039], - [4.050743533053925E7, 4026102.167506821], - [4.05074362170699E7, 4026104.629949201], - [4.050743683431807E7, 4026106.966150228], - [4.050743688561112E7, 4026107.160287504] - ] - }, - "properties": { - "code": "" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050697552118288E7, 4026028.362661481], - [4.050697075787329E7, 4026026.842489458] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050704221346159E7, 4026049.646966941], - [4.050703137036284E7, 4026046.18647901] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050708746004742E7, 4026064.087051627], - [4.050704840096232E7, 4026051.621473066] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05071119278174E7, 4026071.895744022], - [4.050710556055213E7, 4026069.863682419] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050741939071107E7, 4026175.198599438], - [4.05074216811156E7, 4026168.021835575], - [4.050742275893088E7, 4026164.644604296] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050753774654577E7, 4026246.448261945], - [4.050749515081406E7, 4026236.848849251], - [4.050744870329395E7, 4026226.381394062] - ] - }, - "properties": { - "code": "138" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050753774654577E7, 4026246.448261945], - [4.0507613391983E7, 4026263.495786141], - [4.05076192451935E7, 4026264.814870958], - [4.050762119626365E7, 4026265.254565894] - ] - }, - "properties": { - "code": "138" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050742342874898E7, 4026162.545793306], - [4.050743615407222E7, 4026122.672208275] - ] - }, - "properties": { - "code": "L4" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050716820216861E7, 4026089.855066455], - [4.050714461981621E7, 4026082.328947974] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050720821283463E7, 4026102.624334418], - [4.050717462298063E7, 4026091.904402129] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050726278505515E7, 4026114.415332655], - [4.050725934642086E7, 4026115.009285077], - [4.050725586910526E7, 4026115.301280484], - [4.050725237957282E7, 4026115.289096617], - [4.050724890438099E7, 4026114.9728262], - [4.050724546997807E7, 4026114.354876244], - [4.050724210250195E7, 4026113.43994972], - [4.050722536188381E7, 4026108.097315812] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050727312768086E7, 4026112.160285922], - [4.050727144214725E7, 4026112.527790001] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050733913391775E7, 4026097.768664928], - [4.050731882638656E7, 4026102.196402456] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05074011833275E7, 4026084.239767811], - [4.050738651815705E7, 4026087.437277401] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.05074011833275E7, 4026084.239767811], - [4.050740376230329E7, 4026083.794303922], - [4.050740637029002E7, 4026083.5753078], - [4.050740898743934E7, 4026083.584446135], - [4.050741162298967E7, 4026083.825606086] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050744870329395E7, 4026226.381394062], - [4.050744533581797E7, 4026225.466467002], - [4.050744206089737E7, 4026224.261526533], - [4.050743890345625E7, 4026222.775742978], - [4.050743588752466E7, 4026221.020424048], - [4.050743303605565E7, 4026219.00892878], - [4.050743037075064E7, 4026216.756565868], - [4.050742791189419E7, 4026214.280477153], - [4.050742567819968E7, 4026211.599507165], - [4.050742368666689E7, 4026208.734059705], - [4.050742195245258E7, 4026205.705942559], - [4.050742048875517E7, 4026202.538201526], - [4.050741930671428E7, 4026199.254945029], - [4.050741841532595E7, 4026195.88116063], - [4.050741782137419E7, 4026192.442524868], - [4.050741752937933E7, 4026188.965207836], - [4.050741754156362E7, 4026185.475674017], - [4.050741785783435E7, 4026182.000480871], - [4.050741847578449E7, 4026178.566076715], - [4.050741939071107E7, 4026175.198599438] - ] - }, - "properties": { - "code": "" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050684675534101E7, 4025987.268076611], - [4.050685643844293E7, 4025990.358360866], - [4.050689454491971E7, 4026002.519737061] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050703137036284E7, 4026046.18647901], - [4.05070295640735E7, 4026045.610016264], - [4.050701981996215E7, 4026042.500261312], - [4.050697623399237E7, 4026028.590148907], - [4.050697552118288E7, 4026028.362661481] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050704840096232E7, 4026051.621473066], - [4.050704221346159E7, 4026049.646966941] - ] - }, - "properties": { - "code": "F1" - } - }, - { - "type": "Feature", - "geometry": { - "type": "LineString", - "coordinates": [ - [4.050710556055213E7, 4026069.863682419], - [4.050708746004742E7, 4026064.087051627] - ] - }, - "properties": { - "code": "F1" - } - } - ] - } - } -} - -# 飞机数据 - 根据 route.md 配置 -aircraft_data = [ - { - "flightNo": "CA1234", # 根据route.md更新航班号 - "longitude": CA_START["longitude"], - "latitude": CA_START["latitude"], - "time": int(time.time() * 1000), - "altitude": 0.0, - "trackNumber": 1001, - "speed": 50.0, # 根据route.md设置为50km/h - "start_point": CA_START, # 起点 - "end_point": CA_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - }, - { - "flightNo": "MU5123", # 根据route.md更新航班号 - "longitude": MU_START["longitude"], - "latitude": MU_START["latitude"], - "time": int(time.time() * 1000), - "altitude": 0.0, - "trackNumber": 1002, - "speed": 50.0, # 根据route.md设置为50km/h - "start_point": MU_START, # 起点 - "end_point": MU_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - } -] - -# 车辆数据,根据 route.md 配置 -DEFAULT_VEHICLE_SPEED = 30.0 # km/h,默认速度 -EMERGENCY_BRAKE_DECELERATION = 0.8 # 紧急制动减速度 (每次更新减速 80%) -NORMAL_BRAKE_DECELERATION = 0.2 # 正常制动减速度 (每次更新减速 20%) - -vehicle_data = [ - { - "vehicleNo": "鲁B123", # 特勤车,根据route.md更新 - "longitude": SPECIAL_VEHICLE_START["longitude"], - "latitude": SPECIAL_VEHICLE_START["latitude"], - "time": int(time.time() * 1000), - "direction": 0.0, # 方向角 - "speed": 30.0, # 根据route.md设置为30km/h - "start_point": SPECIAL_VEHICLE_START, # 起点 - "end_point": SPECIAL_VEHICLE_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - }, - { - "vehicleNo": "鲁B234", # 普通车,根据route.md更新 - "longitude": NORMAL_VEHICLE_START["longitude"], - "latitude": NORMAL_VEHICLE_START["latitude"], - "time": int(time.time() * 1000), - "direction": 180.0, # 根据route.md设置为180度 - "speed": 30.0, # 根据route.md设置为30km/h - "start_point": NORMAL_VEHICLE_START, # 起点 - "end_point": NORMAL_VEHICLE_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - }, - { - "vehicleNo": "鲁B567", # 无人车A,根据route.md更新 - "longitude": UNMANNED_A_START["longitude"], - "latitude": UNMANNED_A_START["latitude"], - "time": int(time.time() * 1000), - "direction": 90.0, # 根据route.md设置为90度 - "speed": 25.0, # 根据route.md设置为25km/h - "start_point": UNMANNED_A_START, # 起点 - "end_point": UNMANNED_A_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - }, - { - "vehicleNo": "鲁B579", # 无人车B,根据route.md更新 - "longitude": UNMANNED_B_START["longitude"], - "latitude": UNMANNED_B_START["latitude"], - "time": int(time.time() * 1000), - "direction": 270.0, # 根据route.md设置为270度 - "speed": 25.0, # 根据route.md设置为25km/h - "start_point": UNMANNED_B_START, # 起点 - "end_point": UNMANNED_B_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 - } -] - -# 车辆分类 -airport_vehicle_data = [v for v in vehicle_data if v["vehicleNo"] in ["鲁B123", "鲁B234"]] # 特勤车和普通车 -unmanned_vehicle_data = [v for v in vehicle_data if v["vehicleNo"] in ["鲁B567", "鲁B579"]] # 无人车 - -# 添加车辆状态类 -@final -class VehicleState: - vehicle_no: str - is_running: bool - current_command: str | None - command_priority: int - target_speed: float - brake_mode: Literal['emergency', 'normal'] | None - command_reason: str | None - last_command_time: float - traffic_light_state: str | None - target_lat: float | None - target_lon: float | None - is_traffic_light_stop: bool - - def __init__(self, vehicle_no: str) -> None: - self.vehicle_no = vehicle_no - self.is_running = True # 运行状态 - self.current_command = None # 当前执行的指令 - self.command_priority = 0 # 当前指令优先级 - self.target_speed = float(DEFAULT_VEHICLE_SPEED) # 目标速度 - self.brake_mode = None # 制动模式:'emergency' 或 'normal' - self.command_reason = None # 指令原因 - self.last_command_time = float(time.time()) # 最后一次指令时间 - self.traffic_light_state = None # 当前红绿灯状态 - self.target_lat = None # 目标纬度 - self.target_lon = None # 目标经度 - self.is_traffic_light_stop = False # 是否因红灯停车 - self.remoteControlActive = False # 远程控制是否激活 - - def can_be_overridden_by(self, command_type: str) -> bool: - """判断当前指令是否可以被新指令覆盖""" - # 红绿灯信号不参与指令优先级判断 - if command_type in ["RED", "GREEN", "YELLOW"]: - return True - - # 类型安全获取优先级,避免 Unknown | None 作为 dict key - new_priority = COMMAND_PRIORITIES.get(str(command_type), 0) - current_key = self.current_command if isinstance(self.current_command, str) else None - current_priority = COMMAND_PRIORITIES.get(current_key or "", 0) - - # 相同类型的指令可以覆盖(因为需要持续发送) - if isinstance(self.current_command, str) and command_type == self.current_command: - return True - - # ALERT 指令可以被 RESUME 解除 - if self.current_command == "ALERT": - return command_type == "RESUME" - - # WARNING 指令可以被 RESUME 或相同及更高优先级指令覆盖 - if self.current_command == "WARNING": - return command_type == "RESUME" or new_priority >= current_priority - - # 其他情况下,相同或更高优先级可以覆盖 - return new_priority >= current_priority - - def update_command(self, command_type: str, target_lat: float | None = None, target_lon: float | None = None) -> None: - """更新指令状态""" - # 更新目标位置 - if target_lat is not None and target_lon is not None: - self.target_lat = target_lat - self.target_lon = target_lon - - # 如果是红绿灯状态,只更新状态,不影响其他指令 - if command_type in ["RED", "GREEN", "YELLOW"]: - old_state = self.traffic_light_state - self.traffic_light_state = command_type - - # 更新红灯停车状态 - if command_type == "RED": - self.is_traffic_light_stop = True - elif command_type in ["GREEN", "YELLOW"]: - self.is_traffic_light_stop = False - - print(f"车辆 {self.vehicle_no} 收到红绿灯信号: {command_type} (原状态: {old_state})") - else: - # 其他指令正常更新 - self.current_command = command_type - self.command_priority = COMMAND_PRIORITIES.get(command_type, 0) - - # RESUME 指令不清除红绿灯状态,红绿灯状态由信号灯独立控制 - if command_type == "RESUME": - # 清除其他状态 - self.brake_mode = None - self.command_reason = None - - # 更新运行状态 - self.is_running = self.can_move() - print(f"更新车辆 {self.vehicle_no} 状态: command={self.current_command}, traffic_light={self.traffic_light_state}, priority={self.command_priority}, is_running={self.is_running}") - - def can_move(self) -> bool: - """检查车辆是否可以移动""" - # 如果有告警指令,不能移动 - if self.current_command == "ALERT": - return False - - # 如果有预警指令,不能移动 - if self.current_command == "WARNING": - return False - - # 如果有安全停靠指令,不能移动 - if self.current_command == "PARKING": - return False - - # 如果是红灯停车状态,不能移动 - if self.is_traffic_light_stop: - return False - - # 其他情况可以移动 - return True - - def log_state(self) -> None: - """记录当前车辆状态""" - print(f""" -车辆状态: -- 车辆编号: {self.vehicle_no} -- 运行状态: {'运行中' if self.is_running else '已停止'} -- 当前指令: {self.current_command} -- 红绿灯状态: {self.traffic_light_state} -- 红灯停车: {'是' if self.is_traffic_light_stop else '否'} -- 指令优先级: {self.command_priority} -- 目标速度: {self.target_speed} -- 制动模式: {self.brake_mode} -- 指令原因: {self.command_reason} -- 目标位置: ({self.target_lat}, {self.target_lon}) -""") - -# 添加车辆状态管理 -vehicle_states = {} -for vehicle in vehicle_data: - vehicle_states[str(vehicle["vehicleNo"])] = VehicleState(str(vehicle["vehicleNo"])) - -from collections.abc import Mapping - -def calculate_distance_to_target(vehicle: Mapping[str, float], target_lat: float, target_lon: float) -> float: - """计算车辆到目标位的距离(米)""" - # 转换为弧度 - lat1_rad = math.radians(vehicle["latitude"]) - lon1_rad = math.radians(vehicle["longitude"]) - lat2_rad = math.radians(target_lat) - lon2_rad = math.radians(target_lon) - - dlat = lat2_rad - lat1_rad - dlon = lon2_rad - lon1_rad - - # 使用 Haversine 公式计算距离 - a = math.sin(dlat/2) * math.sin(dlat/2) + \ - math.cos(lat1_rad) * math.cos(lat2_rad) * \ - math.sin(dlon/2) * math.sin(dlon/2) - c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) - return EARTH_RADIUS * c - -class RequestVehicleCommandPayload(TypedDict, total=False): - vehicleID: str - commandType: str - commandReason: str - signalState: str - latitude: float - longitude: float - transId: str - -@app.route('/api/VehicleCommandInfo', methods=['POST']) -def handle_vehicle_command(): - # 提前定义并注解,避免未绑定与类型未知 - data: RequestVehicleCommandPayload = {} - try: - data = request.get_json(silent=True) or {} - vehicle_id = data.get("vehicleID") - command_type = data.get("commandType", "").upper() - reason = data.get("commandReason", "").upper() - signal_state = data.get("signalState", "").upper() - target_lat = data.get("latitude", None) - target_lon = data.get("longitude", None) - - print(f"收到车辆控制指令: vehicle_id={vehicle_id}, type={command_type}, reason={reason}, signal_state={signal_state}") - print(f"完整请求数据: {data}") - - # 检查 SIGNAL 类型命令必须包含 signalState - if command_type == "SIGNAL" and not signal_state: - print(f"SIGNAL 类型命令缺少 signalState") - return jsonify({ - "code": 400, - "msg": "SIGNAL command must include signalState", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }), 400 - - # 检查车辆是否存在 - vehicle_state = vehicle_states.get(vehicle_id) - if not vehicle_state: - print(f"未找到车辆: {vehicle_id}") - return jsonify({ - "code": 404, - "msg": f"Vehicle {vehicle_id} not found", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }), 404 - - # 打印当前车辆状态 - print(f"当前车辆状态: vehicle={vehicle_id}") - vehicle_state.log_state() - - # 特勤车(鲁B123)只响应红绿灯信号 - if vehicle_id == "鲁B123": - if command_type != "SIGNAL": - print(f"特勤车辆忽略非红绿灯指令: {vehicle_id}") - return jsonify({ - "code": 200, - "msg": "Special vehicle only responds to traffic light signals", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - # 更新红绿灯状态和指令状态 - vehicle_state.update_command(signal_state, target_lat, target_lon) - print(f"特勤车 {vehicle_id} 更新状态: command={command_type}, traffic_light={vehicle_state.traffic_light_state}") - return jsonify({ - "code": 200, - "msg": "Traffic light state updated", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - - # 检查指令优先级并添加详细日志 - check_command = signal_state if command_type == "SIGNAL" else command_type - can_override = vehicle_state.can_be_overridden_by(check_command) - print(f"指令优先级检查: vehicle={vehicle_id}, current_command={str(vehicle_state.current_command)}, new_command={check_command}, can_override={can_override}") - - if not can_override: - print(f"指令优先级过低: vehicle={vehicle_id}, current_priority={vehicle_state.command_priority}, command={check_command}") - return jsonify({ - "code": 400, - "msg": "Command priority too low", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - - # 处理不同类型的指令 - if command_type == "SIGNAL": - # 更新红绿灯状态和指令状态 - vehicle_state.update_command(signal_state, target_lat, target_lon) - print(f"车辆 {vehicle_id} 更新状态: command={command_type}, traffic_light={vehicle_state.traffic_light_state}") - return jsonify({ - "code": 200, - "msg": "Traffic light state updated", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - elif command_type in ["ALERT", "WARNING"]: - # 查找当前车辆 - current_vehicle = None - for v in vehicle_data: - if v["vehicleNo"] == vehicle_id: - current_vehicle = v - break - - # 执行告警指令直接停车 - print(f"执行{'紧急' if command_type == 'ALERT' else '正常'}制动: vehicle={vehicle_id}") - vehicle_state.current_command = command_type - vehicle_state.command_priority = COMMAND_PRIORITIES.get(command_type, 0) - vehicle_state.is_running = False - vehicle_state.target_speed = 0 - vehicle_state.brake_mode = "emergency" if command_type == "ALERT" else "normal" - vehicle_state.target_lat = target_lat - vehicle_state.target_lon = target_lon - # 注释掉:不再直接设置车辆速度为0,让位置更新逻辑统一处理 - # if current_vehicle: - # current_vehicle["speed"] = 0 - - return jsonify({ - "code": 200, - "msg": "Command executed successfully", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - - elif command_type == "RESUME": - print(f"执行恢复指令: vehicle_id={vehicle_id}") - # 检查当前车辆 - current_vehicle = None - for v in vehicle_data: - if v["vehicleNo"] == vehicle_id: - current_vehicle = v - break - - # 清除限制性指令 - if vehicle_state.current_command in ["ALERT", "WARNING"]: - print(f"清除限制性指令: vehicle={vehicle_id}") - vehicle_state.current_command = None - vehicle_state.command_priority = 0 - vehicle_state.is_running = True - vehicle_state.target_speed = DEFAULT_VEHICLE_SPEED - vehicle_state.brake_mode = None - vehicle_state.target_lat = None - vehicle_state.target_lon = None - - # 更新车辆运行状态 - vehicle_state.is_running = vehicle_state.can_move() - if vehicle_state.is_running and current_vehicle: - print(f"车辆 {vehicle_id} 恢复运行") - # 注释掉:不再直接设置车辆速度,让位置更新逻辑统一处理 - # current_vehicle["speed"] = DEFAULT_VEHICLE_SPEED - - # 记录状态变化但不更新指令 - vehicle_state.command_reason = reason - vehicle_state.last_command_time = time.time() - - print(f"Vehicle {vehicle_id} state updated: running={vehicle_state.is_running}, " - f"command={vehicle_state.current_command}, traffic_light={vehicle_state.traffic_light_state}, " - f"reason={reason}, priority={vehicle_state.command_priority}, " - f"target_speed={vehicle_state.target_speed}, brake_mode={vehicle_state.brake_mode}") - - return jsonify({ - "code": 200, - "msg": "Command executed successfully", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - - return jsonify({ - "code": 200, - "msg": "Command executed successfully", - "transId": data.get("transId"), - "timestamp": int(time.time() * 1000) - }) - - except Exception as e: - print(f"Error handling vehicle command: {str(e)}") - return jsonify({ - "code": 500, - "msg": str(e), - "transId": data.get("transId", ""), - "timestamp": int(time.time() * 1000) - }), 500 - -# 删除了复杂的红绿灯和路口计算函数,因为已简化为往复运动逻辑 - -def update_position_with_vector( - obj: dict[str, Any], - target_point: "Point", - start_point: "Point", - speed: float, - elapsed_time: float, - return_to_start: bool = False -) -> bool: - """ - 基于向量的位置更新逻辑 - - Args: - obj: 需要更新位置的对象(航空器或车辆) - target_point: 目标点坐标 {"latitude": float, "longitude": float} - start_point: 起点坐标 {"latitude": float, "longitude": float} - speed: 移动速度(km/h) - elapsed_time: 经过的时间(秒) - return_to_start: 是否在到达目标点时返回起点 - """ - # 检查是否在等待状态 - if "wait_until" not in obj: - obj["wait_until"] = 0.0 - - current_time = time.time() - # 基于类型安全读取 wait_until,避免 float 与 object 比较 - _wu = to_float_safe(obj.get("wait_until")) - wait_until = 0.0 if _wu is None else float(_wu) - if current_time < wait_until: - return False # 还在等待中,不更新位置 - - # 计算这一步要移动的距离(米)并转换为经纬度 - speed_mps = float(speed) * 1000.0 / 3600.0 - distance = speed_mps * float(elapsed_time) - - # 安全读取并转换当前位置与目标点/起点经纬度为 float - cur_lat = to_float_safe(obj.get("latitude")) - cur_lon = to_float_safe(obj.get("longitude")) - tgt_lat = to_float_safe(target_point.get("latitude")) - tgt_lon = to_float_safe(target_point.get("longitude")) - if cur_lat is None or cur_lon is None or tgt_lat is None or tgt_lon is None: - # 无法解析经纬度,跳过更新 - return False - - # 将移动距离转换为经纬度变化量 - move_dlat, move_dlon = meters_to_degrees(distance, cur_lat) - - # 将5米的判断距离转换为经纬度(优化:从15米减少到5米,减少颤抖) - check_dlat, check_dlon = meters_to_degrees(5.0, cur_lat) - - # 计算当前位置到终点的向量(经纬度空间) - vector_lat = float(tgt_lat) - float(cur_lat) - vector_lon = float(tgt_lon) - float(cur_lon) - - # 计算向量长度(经纬度空间) - vector_length = math.sqrt(vector_lat * vector_lat + vector_lon * vector_lon) - - # 检查是否到达终点 - if vector_length <= math.sqrt(check_dlat * check_dlat + check_dlon * check_dlon): - # 精确设置到目标点位置,避免位置偏差 - obj["latitude"] = float(tgt_lat) - obj["longitude"] = float(tgt_lon) - - if return_to_start: - # 返回起点并设置等待时间 - obj["wait_until"] = current_time + WAIT_TIME_AFTER_RETURN - print(f"对象 {obj.get('flightNo', obj.get('vehicleNo'))} 返回起点,等待{WAIT_TIME_AFTER_RETURN}秒后继续") - else: - # 设置短暂等待时间,避免立即切换方向导致的颤抖 - obj["wait_until"] = current_time + 0.1 # 0.1秒的短暂等待 - print(f"对象 {obj.get('flightNo', obj.get('vehicleNo'))} 到达目标点,等待0.1秒后切换方向") - return True # 表示已到达终点 - else: - # 归一化方向向量 - unit_lat = vector_lat / vector_length - unit_lon = vector_lon / vector_length - - # 计算这一步的位置变化 - dlat = move_dlat * unit_lat - dlon = move_dlon * unit_lon - - # 更新位置(写回为 float) - obj["latitude"] = float(cur_lat + dlat) - obj["longitude"] = float(cur_lon + dlon) - return False # 表示正在移动中 - -def update_object_position(obj: dict[str, Any], elapsed_time: float) -> None: - """统一的位置更新函数 - 用于飞机和车辆的往复运动""" - # 获取对象类型和标识 - obj_type = "航空器" if "flightNo" in obj else "车辆" - obj_id = obj.get("flightNo", obj.get("vehicleNo")) - - # 根据移动方向确定目标点 - if obj["moving_to_end"]: - target_raw = obj["end_point"] - start_raw = obj["start_point"] - else: - target_raw = obj["start_point"] - start_raw = obj["end_point"] - - # 构造类型安全的 Point - t_lat = to_float_safe(target_raw.get("latitude")) if isinstance(target_raw, dict) else None - t_lon = to_float_safe(target_raw.get("longitude")) if isinstance(target_raw, dict) else None - s_lat = to_float_safe(start_raw.get("latitude")) if isinstance(start_raw, dict) else None - s_lon = to_float_safe(start_raw.get("longitude")) if isinstance(start_raw, dict) else None - if t_lat is None or t_lon is None or s_lat is None or s_lon is None: - # 无法解析坐标,直接更新时间戳后返回 - obj["time"] = int(time.time() * 1000) - return - - target_point: Point = {"latitude": float(t_lat), "longitude": float(t_lon)} - start_point: Point = {"latitude": float(s_lat), "longitude": float(s_lon)} - - # 读取速度为 float - spd = to_float_safe(obj.get("speed")) - if spd is None: - spd = 0.0 - - # 更新位置 - reached_target = update_position_with_vector( - obj, - target_point, - start_point, - float(spd), - float(elapsed_time), - return_to_start=False - ) - - if reached_target: - # 切换移动方向 - obj["moving_to_end"] = not obj["moving_to_end"] - target_name = "终点" if not obj["moving_to_end"] else "起点" - print(f"{obj_type} {obj_id} 到达{'终点' if obj['moving_to_end'] else '起点'},开始前往{target_name}") - # 注意:等待时间已经在update_position_with_vector中设置了,不需要重复设置 - - # 更新时间戳 - obj["time"] = int(time.time() * 1000) - -def update_aircraft_position(aircraft: dict[str, Any], elapsed_time: float) -> None: - """更新航空器位置 - 使用统一的位置更新函数""" - update_object_position(aircraft, elapsed_time) - -def update_vehicle_position(vehicle: dict[str, Any], elapsed_time: float) -> None: - """更新车辆位置 - 使用统一的位置更新函数""" - update_object_position(vehicle, elapsed_time) - -# 简化的路口信息(保留API兼容性) -INTERSECTIONS = { - "TL001": { - "id": "INT001", - "name": "路口1", - "latitude": 36.369696, - "longitude": 120.083084 - }, - "TL002": { - "id": "INT002", - "name": "路口2", - "latitude": 36.365617, - "longitude": 120.084637 - } -} - -# T6路口定义 -T6_INTERSECTION = { - "id": "T6", - "name": "T6路口", - "latitude": 36.372000, - "longitude": 120.085000 -} - -def generate_traffic_light_data() -> list[dict[str, Any]]: - """生成红绿灯数据""" - traffic_light_data = [] - - # 生成两个固定路口的红绿灯数据 - for tl_id, intersection in INTERSECTIONS.items(): - signal = { - "id": tl_id, - "state": 1, # 0: RED, 1: GREEN,测试时保持绿灯 - "timestamp": int(time.time() * 1000), - "intersection": intersection["id"], - "position": { - "latitude": intersection["latitude"], - "longitude": intersection["longitude"] - } - } - traffic_light_data.append(signal) - - return traffic_light_data - -# 红绿灯数据 -traffic_light_data = generate_traffic_light_data() - -# 分别记录航空器、机场车辆和无人车的上次更新时间 -last_aircraft_update_time = time.time() -last_vehicle_update_time = time.time() # 机场车辆更新时间 -last_unmanned_vehicle_update_time = time.time() # 无人车更新时间 -last_light_switch_time = time.time() - -def check_auth() -> bool: - auth_header = request.headers.get('Authorization') - - if not auth_header: - print("认证失败: 缺少Authorization头") - return False - - # 直接比较完整的Authorization头 - result = auth_header == AUTH_TOKEN - if not result: - print(f"认证失败: token不匹配") - print(f"收到的token: {auth_header}") - print(f"期望的token: {AUTH_TOKEN}") - return result - -def to_float_safe(x: object) -> float | None: - """ - 安全将值转换为 float: - - 允许 int/float - - 允许非布尔的数字字符串 - - 允许 dict,优先取 'value'、'longitude'、'latitude' 等常见键 - - 无法解析则返回 None - """ - # 排除布尔值(bool 是 int 的子类,但不应当被视为数值) - if isinstance(x, bool): - return None - if isinstance(x, (int, float)): - return float(x) - if isinstance(x, str): - try: - return float(x.strip()) - except Exception: - return None - if isinstance(x, dict): - for key in ("value", "longitude", "latitude"): - if key in x: - return to_float_safe(x[key]) - return None - return None - -@app.route('/openApi/getCurrentFlightPositions', methods=['GET', 'OPTIONS']) -def get_flight_positions(): - """获取当前航空器位置信息""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - global last_aircraft_update_time - current_time = time.time() - elapsed_time = current_time - last_aircraft_update_time - - # 只在达到更新间隔时更新位置 - if elapsed_time >= UPDATE_INTERVAL: - for aircraft in aircraft_data: - update_aircraft_position(aircraft, UPDATE_INTERVAL) - aircraft["time"] = int(current_time * 1000) - last_aircraft_update_time = current_time - - # 创建符合 API 格式的响应数据 - response_data = [] - for aircraft in aircraft_data: - lon_val = to_float_safe(aircraft.get("longitude")) - lat_val = to_float_safe(aircraft.get("latitude")) - api_aircraft = { - "flightNo": aircraft.get("flightNo"), - "longitude": round(lon_val, 6) if lon_val is not None else aircraft.get("longitude"), - "latitude": round(lat_val, 6) if lat_val is not None else aircraft.get("latitude"), - "time": aircraft.get("time") - } - response_data.append(api_aircraft) - - return jsonify({ - "status": 200, - "msg": "当前航空器实时位置数据", - "data": response_data - }) - -def switch_traffic_light_state() -> None: - """统一处理红绿灯状态切换""" - global last_light_switch_time - current_time = time.time() - - # 西路口红绿灯每15秒切换一次 - elapsed_since_switch = current_time - last_light_switch_time - if elapsed_since_switch >= TRAFFIC_LIGHT_SWITCH_INTERVAL: # 使用常量 - # 获取当前状态 - current_state = traffic_light_data[0]["state"] - - # 根据当前状态决定下一个状态 - if current_state == 0: # 当前是红灯 - # 切换到黄灯(表示红绿灯故障) - traffic_light_data[0]["state"] = 2 # 2 表示黄灯 - print(f"西路口红绿灯状态切换为: 黄灯(红绿灯故障)") - elif current_state == 2: # 当前是黄灯 - # 从黄灯切换到绿灯 - traffic_light_data[0]["state"] = 1 # 1 表示绿灯 - print(f"西路口红绿灯状态切换为: 绿灯") - else: # 当前是绿灯 - # 从绿灯切换到红灯 - traffic_light_data[0]["state"] = 0 # 0 表示红灯 - print(f"西路口红绿灯状态切换为: 红灯") - - last_light_switch_time = current_time - - # 更新东路口红绿灯(根据航空器位置) - if aircraft_data: - aircraft = aircraft_data[0] - # 安全获取并转换纬度为浮点数,避免类型错误 - aircraft_lat = to_float_safe(aircraft.get("latitude")) - t6_lat = to_float_safe(T6_INTERSECTION.get("latitude")) - if aircraft_lat is not None and t6_lat is not None: - lat_diff = abs(aircraft_lat - t6_lat) * 111319.9 # 转米 - else: - # 无法解析时默认认为距离较大,避免误判为红灯 - lat_diff = float('inf') - - old_state = traffic_light_data[1]["state"] - # 大于50米为绿灯(1),否则红灯(0) - traffic_light_data[1]["state"] = 1 if lat_diff > DIST_50M else 0 - - if old_state != traffic_light_data[1]["state"]: - print(f"东路口红绿灯状态切换为: {'绿灯' if traffic_light_data[1]['state'] == 1 else '红灯'}") - -@app.route('/openApi/getCurrentVehiclePositions', methods=['GET', 'OPTIONS']) -def get_vehicle_positions(): - """获取当前车辆位置信息(仅机场车辆)""" - if request.method == 'OPTIONS': - return '', 204 - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - global last_vehicle_update_time - current_time = time.time() - elapsed_time = current_time - last_vehicle_update_time - try: - # 只在达到更新间隔时更新位置 - if elapsed_time >= UPDATE_INTERVAL: - for vehicle in airport_vehicle_data: - update_vehicle_position(vehicle, UPDATE_INTERVAL) - vehicle["time"] = int(current_time * 1000) - last_vehicle_update_time = current_time - response_data = [] - for v in airport_vehicle_data: - lon_val = to_float_safe(v.get("longitude")) - lat_val = to_float_safe(v.get("latitude")) - v_out = { - "vehicleNo": v.get("vehicleNo"), - "longitude": round(lon_val, 6) if lon_val is not None else v.get("longitude"), - "latitude": round(lat_val, 6) if lat_val is not None else v.get("latitude"), - "time": v.get("time") - } - response_data.append(v_out) - return jsonify({ - "status": 200, - "msg": "当前车辆实时位置数据", - "data": response_data - }) - except Exception as e: - print(f"Error in get_vehicle_positions: {str(e)}") - return jsonify({ - "status": 500, - "msg": str(e), - "data": None - }), 500 - -@app.route('/openApi/getTrafficLightSignals', methods=['GET', 'OPTIONS']) -def get_traffic_light_signals(): - """获取红绿灯信号状态 (旧的轮询接口)""" - if request.method == 'OPTIONS': - return '', 204 - - # 新增: 返回空数据,以禁用轮询数据源 - return jsonify({ - "status": 200, - "msg": "Traffic light polling endpoint disabled to test push mechanism.", - "data": [] - }) - -@app.after_request -def add_cors_headers(response): - response.headers['Access-Control-Allow-Origin'] = '*' - response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' - response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' - response.headers['Access-Control-Max-Age'] = '3600' - return response - -def check_vehicle_states(): - """定期检查所有车辆状态""" - for vehicle_no, state in vehicle_states.items(): - state.log_state() - -# 认证相关配置 -AUTH_USERNAME = "dianxin" -AUTH_PASSWORD = "dianxin@123" -AUTH_TOKEN = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" - -@app.route('/login', methods=['POST', 'OPTIONS']) -def login(): - if request.method == 'OPTIONS': - return '', 204 - - # 支持多种方式获取用户名和密码 - username = None - password = None - - # 优先从 URL query parameters 中获取(符合API文档示例) - username = request.args.get('username') - password = request.args.get('password') - - # 如果query参数中没有,尝试从form data中获取 - if not username or not password: - username = request.form.get('username') or username - password = request.form.get('password') or password - - # 如果form data中也没有,尝试从JSON body中获取 - if not username or not password: - try: - json_data = request.get_json(silent=True) - if json_data: - username = json_data.get('username') or username - password = json_data.get('password') or password - except: - pass - - print(f"收到登录请求: username={username}, password={password}") - print(f"请求 URL: {request.url}") - print(f"请求方法: {request.method}") - print(f"请求参数: {request.args}") - print(f"请求表单: {request.form}") - print(f"请求JSON: {request.get_json(silent=True)}") - - if not username or not password: - return jsonify({ - "status": 400, - "msg": "缺少用户名或密码", - "data": None - }), 400 - - if username == AUTH_USERNAME and password == AUTH_PASSWORD: - return jsonify({ - "status": 200, - "msg": "登入成功", - "data": AUTH_TOKEN - }) - else: - return jsonify({ - "status": 401, - "msg": "用户名或密码错误", - "data": None - }), 401 - -@app.route('/openApi/getVehicleStatus', methods=['GET', 'OPTIONS']) -def get_vehicle_status(): - """获取无人车状态信息""" - if request.method == 'OPTIONS': - return '', 204 - - vehicle_id = request.args.get('vehicleId') - if not vehicle_id: - return jsonify({ - "status": 400, - "msg": "缺少 vehicleId 参数", - "data": None - }), 400 - - # 查找对应的车辆 - vehicle_state = vehicle_states.get(vehicle_id) - if not vehicle_state: - return jsonify({ - "status": 404, - "msg": f"未找到车辆 {vehicle_id}", - "data": None - }), 404 - - # 返回车辆状态 - return jsonify({ - "status": 200, - "msg": "获取车辆状态成功", - "data": { - "vehicleId": vehicle_id, - "status": "NORMAL" if vehicle_state.is_running else "STOPPED", - "command": vehicle_state.current_command, - "commandPriority": vehicle_state.command_priority, - "trafficLightState": vehicle_state.traffic_light_state, - "timestamp": int(time.time() * 1000) - } - }) - -# 无人车位置数据生成函数 -def generate_unmanned_vehicle_location_data() -> list[dict[str, Any]]: - """生成无人车位置数据,符合官方API格式""" - unmanned_vehicles = [] - - # 从现有vehicle_data中筛选无人车(QN开头的车辆) - for vehicle in unmanned_vehicle_data: - lon_val = to_float_safe(vehicle.get("longitude")) - lat_val = to_float_safe(vehicle.get("latitude")) - location_info = { - "transId": str(uuid.uuid4()), - "timestamp": int(time.time() * 1000), - "vehicleID": vehicle["vehicleNo"], - "longitude": round(lon_val, 6) if lon_val is not None else vehicle.get("longitude"), - "latitude": round(lat_val, 6) if lat_val is not None else vehicle.get("latitude") - } - unmanned_vehicles.append(location_info) - - return unmanned_vehicles - -# 生成符合API规范的完整车辆状态数据 -def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]: - """生成符合universal_autonomous_vehicle_api规范的完整车辆状态数据""" - current_time = time.time() - timestamp_ms = int(current_time * 1000) - - # 获取车辆基础数据 - vehicle_data_item = None - for v in unmanned_vehicle_data: - if v["vehicleNo"] == vehicle_id: - vehicle_data_item = v - break - - if not vehicle_data_item: - return {} - - # 获取车辆状态 - vehicle_state = vehicle_states.get(vehicle_id) - - # 基础位置和运动数据 - lat_val = to_float_safe(vehicle_data_item.get("latitude")) - lon_val = to_float_safe(vehicle_data_item.get("longitude")) - speed_val = to_float_safe(vehicle_data_item.get("speed", 0)) - direction_val = to_float_safe(vehicle_data_item.get("direction", 0)) - - # 生成完整状态数据 - status_data = { - "vehicleInfo": { - "vehicleId": vehicle_id - }, - "operationalStatus": { - "powerStatus": "ON", - "systemHealth": "HEALTHY" if vehicle_state and vehicle_state.is_running else "DEGRADED", - "operationalMode": "AUTONOMOUS", - "emergencyStatus": "CRITICAL" if vehicle_state and vehicle_state.current_command == "ALERT" - else "WARNING" if vehicle_state and vehicle_state.current_command == "WARNING" - else "NORMAL", - "lastHeartbeat": timestamp_ms - }, - "controlStatus": { - "controlMode": "AUTONOMOUS", - "controlAuthority": "SYSTEM", - "remoteControlActive": vehicle_state.remoteControlActive if vehicle_state else False - }, - "motionStatus": { - "position": { - "latitude": round(lat_val, 6) if lat_val is not None else 0.0, - "longitude": round(lon_val, 6) if lon_val is not None else 0.0 - }, - "velocity": { - "speed": round(speed_val * 1000 / 3600, 2) if speed_val is not None else 0.0, # 转换为 m/s - "direction": round(math.radians(direction_val), 4) if direction_val is not None else 0.0 # 转换为弧度 - } - }, - "safetyStatus": { - "collisionAvoidanceActive": vehicle_state.is_running if vehicle_state else True, - "emergencyBrakingReady": True, - "pathPlanningStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE", - "obstacleDetectionStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE", - "minimumRiskManeuverTriggered": vehicle_state.current_command == "ALERT" if vehicle_state else False - }, - "sensorStatus": { - "gps": { - "status": "ACTIVE", - "accuracy": 0.5, # 0.5米精度 - "lastUpdate": timestamp_ms - } - }, - "batteryStatus": { - "mainBattery": { - "chargeLevel": 85.5, # 85.5% - "voltage": 48.2, # 48.2V - "current": -15.3, # -15.3A (放电) - "temperature": 35.2, # 35.2°C - "chargingStatus": "DISCHARGING" - } - }, - "communicationStatus": { - "v2xStatus": "CONNECTED", - "cellularSignalStrength": -65, # -65dBm - "wifiStatus": "CONNECTED", - "cloudConnectivity": "ONLINE" - } - } - - return status_data - -def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]: - """根据fields参数过滤车辆状态字段""" - if not fields: - return status_data - - # 解析fields参数 - requested_fields = [f.strip() for f in fields.split(',')] - filtered_data = {} - - # 可用的字段组 - available_fields = { - "vehicleInfo", "operationalStatus", "controlStatus", "motionStatus", - "safetyStatus", "sensorStatus", "batteryStatus", "communicationStatus" - } - - for field in requested_fields: - if field in available_fields and field in status_data: - filtered_data[field] = status_data[field] - - return filtered_data - -# 无人车状态数据生成函数 -def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]: - """生成无人车状态数据,符合官方API格式""" - vehicle_states_data = [] - - if is_single and vehicle_id: - # 单个车辆状态查询 - vehicle_state = vehicle_states.get(vehicle_id) - if vehicle_state: - state_info = { - "transId": str(uuid.uuid4()), - "timestamp": int(time.time() * 1000), - "vehicleID": vehicle_id, - "loginState": True, - "faultInfo": [], - "activeSafety": not vehicle_state.is_running, - "RC": False, - "Command": 1 if vehicle_state.current_command == "ALERT" else 0, - "airportInfo": [], - "vehicleMode": 2, # 自动模式 - "gearState": 2, # D档 - "chassisReady": vehicle_state.is_running, - "collisionStatus": False, - "clearance": 1 if vehicle_state.is_running else 0, - "turnSignalStstus": 0, - "pointCloud": [] - } - vehicle_states_data.append(state_info) - else: - # 所有车辆状态查询 - for vehicle in unmanned_vehicle_data: - vehicle_state = vehicle_states.get(vehicle["vehicleNo"]) - state_info = { - "transId": str(uuid.uuid4()), - "timestamp": int(time.time() * 1000), - "vehicleID": vehicle["vehicleNo"], - "loginState": True, - "faultInfo": [], - "activeSafety": not vehicle_state.is_running if vehicle_state else False, - "RC": False, - "Command": 1 if vehicle_state and vehicle_state.current_command == "ALERT" else 0, - "airportInfo": [], - "vehicleMode": 2, - "gearState": 2, - "chassisReady": vehicle_state.is_running if vehicle_state else True, - "collisionStatus": False, - "clearance": 1 if vehicle_state and vehicle_state.is_running else 0, - "turnSignalStstus": 0, - "pointCloud": [] - } - vehicle_states_data.append(state_info) - - return vehicle_states_data - -# 无人车API接口 -@app.route('/api/VehicleLocationInfo', methods=['GET', 'OPTIONS']) -def get_unmanned_vehicle_location(): - """无人车位置上报接口""" - if request.method == 'OPTIONS': - return '', 204 - try: - current_time = time.time() - global last_unmanned_vehicle_update_time - elapsed_time = current_time - last_unmanned_vehicle_update_time - if elapsed_time >= UPDATE_INTERVAL: - for vehicle in unmanned_vehicle_data: - update_vehicle_position(vehicle, UPDATE_INTERVAL) - vehicle["time"] = int(current_time * 1000) - last_unmanned_vehicle_update_time = current_time - # 生成无人车位置数据 - location_data = generate_unmanned_vehicle_location_data() - - return jsonify(location_data) - except Exception as e: - print(f"Error in get_unmanned_vehicle_location: {str(e)}") - return jsonify([]), 500 - -@app.route('/api/VehicleStateInfo', methods=['POST', 'OPTIONS']) -def get_unmanned_vehicle_state(): - """无人车状态查询接口""" - if request.method == 'OPTIONS': - return '', 204 - - try: - data = request.json - vehicle_id = data.get("vehicleID") if data else None - is_single = data.get("isSingle", True) if data else False - - print(f"收到无人车状态查询请求: vehicle_id={vehicle_id}, is_single={is_single}") - - # 生成无人车状态数据 - state_data = generate_unmanned_vehicle_state_data(vehicle_id, is_single) - - print(f"返回无人车状态数据,数量: {len(state_data)}") - return jsonify(state_data) - - except Exception as e: - print(f"Error in get_unmanned_vehicle_state: {str(e)}") - return jsonify([]), 500 - -def update_ca3456_status() -> None: - """更新CA3456航空器状态 - 进港->停留1分钟->出港循环""" - global ca3456_status - current_time = time.time() - # 安全转换状态起始时间,避免与 float 相减的类型冲突 - status_start_time = to_float_safe(ca3456_status.get("status_start_time")) - if status_start_time is None: - # 无法解析时,用当前时间作为起点,避免负值 - status_start_time = current_time - ca3456_status["status_start_time"] = status_start_time - elapsed_time = float(current_time) - float(status_start_time) - - # 读取并安全转换周期与阶段时长,避免 str | int | float 与 float 的 %/比较类型问题 - cycle_duration = to_float_safe(ca3456_status.get("cycle_duration")) - arrival_duration = to_float_safe(ca3456_status.get("arrival_duration")) - wait_duration = to_float_safe(ca3456_status.get("wait_duration")) - - # 兜底默认值:若无法解析则使用已有注释中的默认秒数 - if cycle_duration is None or cycle_duration <= 0: - cycle_duration = 77.0 - if arrival_duration is None or arrival_duration < 0: - arrival_duration = 30.0 - if wait_duration is None or wait_duration < 0: - wait_duration = 15.0 - - # 计算当前在循环中的位置(均为 float) - cycle_time = float(elapsed_time) % float(cycle_duration) - - if cycle_time < arrival_duration: - # 进港阶段 - ca3456_status["type"] = "IN" - logging.info(f"CA3456 进港阶段: {cycle_time:.1f}s") - elif cycle_time < (arrival_duration + wait_duration): - # 停留阶段 - ca3456_status["type"] = "ARRIVED" - logging.info(f"CA3456 停留阶段: {cycle_time:.1f}s") - else: - # 出港阶段 - ca3456_status["type"] = "OUT" - logging.info(f"CA3456 出港阶段: {cycle_time:.1f}s") - - ca3456_status["timestamp"] = int(current_time * 1000) - -# 新增API端点 - -@app.route('/userInfoController/refreshToken', methods=['GET', 'OPTIONS']) -def refresh_token(): - """Token刷新接口""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - - # 返回新的token(实际上还是同一个) - return jsonify({ - "status": 200, - "msg": "Token刷新成功", - "data": { - "token": "Bearer dianxin-token-2024", - "expiresIn": 86400 # 24小时过期 - } - }) - -@app.route('/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat', methods=['GET', 'OPTIONS']) -def get_arrival_taxiway_route(): - """获取进港滑行路线""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - - # 获取请求参数 - in_runway = request.args.get('inRunway', '35') - out_runway = request.args.get('outRunway', '34') - contact_cross = request.args.get('contactCross', 'F1') - seat = request.args.get('seat', '138') - - logging.info(f"进港路线查询: inRunway={in_runway}, outRunway={out_runway}, contactCross={contact_cross}, seat={seat}") - - # 返回进港路线数据 - return jsonify({ - "status": 200, - "msg": "进港滑行路线查询成功", - "data": aircraft_routes["arrival"] - }) - -@app.route('/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat', methods=['GET', 'OPTIONS']) -def get_departure_taxiway_route(): - """获取出港滑行路线""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - - # 获取请求参数 - in_runway = request.args.get('inRunway', '35') - out_runway = request.args.get('outRunway', '34') - start_seat = request.args.get('startSeat', '138') - - logging.info(f"出港路线查询: inRunway={in_runway}, outRunway={out_runway}, startSeat={start_seat}") - - # 返回出港路线数据 - return jsonify({ - "status": 200, - "msg": "出港滑行路线查询成功", - "data": aircraft_routes["departure"] - }) - -def get_active_flights() -> list[dict[str, Any]]: - """获取当前活跃的进出港航班(简化版)""" - current_time = time.time() - active_flights = [] - - for flight_no, flight_info in flight_data.items(): - # 安全读取并转换为数值 - cycle_start = to_float_safe(flight_info.get("cycle_start")) - active_duration = to_float_safe(flight_info.get("active_duration")) - gap_duration = to_float_safe(flight_info.get("gap_duration")) - - # 兜底默认值 - if cycle_start is None: - cycle_start = current_time - if active_duration is None or active_duration <= 0: - active_duration = 600.0 - if gap_duration is None or gap_duration < 0: - gap_duration = 10.0 - - # 计算从周期开始的时间与总周期 - elapsed_time = float(current_time) - float(cycle_start) - total_cycle = float(active_duration) + float(gap_duration) - - # 避免除零 - if total_cycle <= 0: - total_cycle = float(active_duration) if active_duration > 0 else 1.0 - - # 计算在当前周期中的位置 - cycle_position = float(elapsed_time) % float(total_cycle) - - # 检查是否在活跃期内 - if cycle_position <= float(active_duration): - # 在活跃期内,直接返回固定信息 - flight_data_response = { - "flightNo": flight_info.get("flightNo"), - "type": flight_info.get("type"), - "runway": flight_info.get("runway"), - "contactCross": flight_info.get("contactCross"), - "seat": flight_info.get("seat"), - "time": flight_info.get("fixed_time") # 使用固定时间 - } - active_flights.append(flight_data_response) - - event_type = "落地" if flight_info.get("type") == "IN" else "滑出" - logging.info(f"航班 {flight_no} {event_type}通知中 (活跃期: {cycle_position:.1f}s/{int(active_duration)}s)") - else: - # 在间隔期内,更新固定时间为下一个周期 - cycle_number = int(float(elapsed_time) // float(total_cycle)) + 1 - next_cycle_start = float(cycle_start) + cycle_number * float(total_cycle) - if flight_info.get("type") == "OUT": - flight_info["fixed_time"] = int((next_cycle_start + 30.0) * 1000) # 出港滑出时间 - else: - flight_info["fixed_time"] = int((next_cycle_start + 20.0) * 1000) # 进港落地时间 - - gap_part = cycle_position - float(active_duration) - logging.info(f"航班 {flight_no} 在间隔期,准备下一个周期 (间隔期: {gap_part:.1f}s/{int(gap_duration)}s)") - - return active_flights - -@app.route('/openApi/getInboundAndOutboundFlightsNotification', methods=['GET', 'OPTIONS']) -def get_flights(): - """进出港航班查询接口(Mock)""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - - try: - # 获取当前活跃航班 - active_flights = get_active_flights() - - logging.info(f"进出港航班查询: 返回 {len(active_flights)} 个活跃航班") - - return jsonify({ - "status": 200, - "msg": "进出港航班查询成功", - "data": active_flights - }) - - except Exception as e: - logging.error(f"进出港航班查询失败: {str(e)}") - return jsonify({ - "status": 500, - "msg": f"查询失败: {str(e)}", - "data": [] - }), 500 - -@app.route('/aircraftStatusController/getAircraftStatus', methods=['GET', 'OPTIONS']) -def get_aircraft_status(): - """获取航空器状态""" - if request.method == 'OPTIONS': - return '', 204 - - if not check_auth(): - return jsonify({ - "status": 401, - "msg": "认证失败", - "data": None - }), 401 - - # 更新CA3456状态 - update_ca3456_status() - - # 返回当前状态 - return jsonify({ - "status": 200, - "msg": "航空器状态查询成功", - "data": { - "type": ca3456_status["type"], - "flightNo": ca3456_status["flightNo"], - "inRunway": ca3456_status["inRunway"], - "outRunway": ca3456_status["outRunway"], - "contactCross": ca3456_status["contactCross"], - "seat": ca3456_status["seat"], - "timestamp": ca3456_status["timestamp"] - } - }) - -@app.route('/api/v1/vehicles//status', methods=['GET', 'OPTIONS']) -def get_vehicle_status_universal(vehicle_id): - """通用无人车状态上报接口 - 符合universal_autonomous_vehicle_api规范""" - if request.method == 'OPTIONS': - return '', 204 - - logging.info(f"🔍 收到车辆状态查询请求: vehicleId={vehicle_id}") - - # JWT认证检查 - if not check_auth(): - logging.warning(f"❌ 车辆状态查询认证失败: vehicleId={vehicle_id}") - return jsonify({ - "code": 401, - "message": "认证失败", - "timestamp": int(time.time() * 1000), - "data": None - }), 401 - - # 检查vehicleId格式(字母数字 3~20 位) - if not vehicle_id or len(vehicle_id) < 3 or len(vehicle_id) > 20: - return jsonify({ - "code": 400, - "message": "Bad Request", - "timestamp": int(time.time() * 1000), - "error": { - "type": "VALIDATION_ERROR", - "details": "vehicleId must be 3-20 alphanumeric characters", - "field": "vehicleId" - } - }), 400 - - # 检查车辆是否存在 - vehicle_exists = False - for v in unmanned_vehicle_data: - if v["vehicleNo"] == vehicle_id: - vehicle_exists = True - break - - if not vehicle_exists: - return jsonify({ - "code": 404, - "message": "Not Found", - "timestamp": int(time.time() * 1000), - "error": { - "type": "RESOURCE_NOT_FOUND", - "details": f"Vehicle {vehicle_id} not found", - "field": "vehicleId" - } - }), 404 - - try: - # 更新车辆位置数据 - current_time = time.time() - global last_unmanned_vehicle_update_time - elapsed_time = current_time - last_unmanned_vehicle_update_time - if elapsed_time >= UPDATE_INTERVAL: - for vehicle in unmanned_vehicle_data: - update_vehicle_position(vehicle, UPDATE_INTERVAL) - vehicle["time"] = int(current_time * 1000) - last_unmanned_vehicle_update_time = current_time - - # 生成完整的车辆状态数据 - status_data = generate_comprehensive_vehicle_status(vehicle_id) - if not status_data: - return jsonify({ - "code": 500, - "message": "Internal Server Error", - "timestamp": int(time.time() * 1000), - "error": { - "type": "DATA_ERROR", - "details": "Failed to generate vehicle status data", - "field": None - } - }), 500 - - # 处理fields查询参数 - fields = request.args.get('fields') - if fields: - status_data = filter_vehicle_status_fields(status_data, fields) - - # 返回成功响应 - logging.info(f"🚗 返回车辆状态数据: vehicleId={vehicle_id}, 电量={status_data.get('batteryStatus', {}).get('mainBattery', {}).get('chargeLevel', '未知')}%") - return jsonify({ - "code": 200, - "message": "success", - "timestamp": int(time.time() * 1000), - "data": status_data - }) - - except Exception as e: - logging.error(f"Error in get_vehicle_status_universal: {str(e)}") - return jsonify({ - "code": 500, - "message": "Internal Server Error", - "timestamp": int(time.time() * 1000), - "error": { - "type": "SERVER_ERROR", - "details": str(e), - "field": None - } - }), 500 - -if __name__ == '__main__': - app.run(host='localhost', port=8090, debug=True) \ No newline at end of file diff --git a/tools/mock_unmanned_vehicle.py b/tools/mock_unmanned_vehicle.py index c49783fd..70de8ae7 100644 --- a/tools/mock_unmanned_vehicle.py +++ b/tools/mock_unmanned_vehicle.py @@ -181,6 +181,12 @@ def update_position_with_vector( speed_mps = float(speed) * 1000.0 / 3600.0 distance = speed_mps * float(elapsed_time) + # 累计里程到总里程中 + if "total_mileage" not in obj: + obj["total_mileage"] = 0.0 + obj["total_mileage"] = to_float_safe(obj.get("total_mileage", 0.0)) or 0.0 + obj["total_mileage"] += distance + # 安全读取并转换当前位置与目标点/起点经纬度为 float cur_lat = to_float_safe(obj.get("latitude")) cur_lon = to_float_safe(obj.get("longitude")) @@ -287,28 +293,39 @@ def update_vehicle_position(vehicle: dict[str, Any], elapsed_time: float) -> Non update_object_position(vehicle, elapsed_time) # 无人车数据配置 +current_timestamp = time.time() unmanned_vehicle_data = [ { "vehicleNo": "鲁B567", # 无人车A "longitude": UNMANNED_A_START["longitude"], "latitude": UNMANNED_A_START["latitude"], - "time": int(time.time() * 1000), + "time": int(current_timestamp * 1000), "direction": 90.0, # 90度 "speed": 25.0, # 25km/h "start_point": UNMANNED_A_START, # 起点 "end_point": UNMANNED_A_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 + "moving_to_end": True, # 当前是否向终点移动 + # 任务和里程相关字段 + "total_mileage": 0.0, # 总里程(米) + "mission_start_time": int(current_timestamp * 1000), # 任务开始时间 + "mission_id": "MISSION_A_001", # 任务ID + "mission_type": "PATROL_TRANSPORT" # 任务类型:巡逻运输 }, { "vehicleNo": "鲁B579", # 无人车B "longitude": UNMANNED_B_START["longitude"], "latitude": UNMANNED_B_START["latitude"], - "time": int(time.time() * 1000), + "time": int(current_timestamp * 1000), "direction": 270.0, # 270度 "speed": 25.0, # 25km/h "start_point": UNMANNED_B_START, # 起点 "end_point": UNMANNED_B_END, # 终点 - "moving_to_end": True # 当前是否向终点移动 + "moving_to_end": True, # 当前是否向终点移动 + # 任务和里程相关字段 + "total_mileage": 0.0, # 总里程(米) + "mission_start_time": int(current_timestamp * 1000), # 任务开始时间 + "mission_id": "MISSION_B_001", # 任务ID + "mission_type": "CARGO_TRANSPORT" # 任务类型:货物运输 } ] @@ -473,24 +490,6 @@ class RequestVehicleCommandPayload(TypedDict, total=False): longitude: float transId: str -# 无人车位置数据生成函数 -def generate_unmanned_vehicle_location_data() -> list[dict[str, Any]]: - """生成无人车位置数据,符合官方API格式""" - unmanned_vehicles = [] - - for vehicle in unmanned_vehicle_data: - lon_val = to_float_safe(vehicle.get("longitude")) - lat_val = to_float_safe(vehicle.get("latitude")) - location_info = { - "transId": str(uuid.uuid4()), - "timestamp": int(time.time() * 1000), - "vehicleID": vehicle["vehicleNo"], - "longitude": round(lon_val, 6) if lon_val is not None else vehicle.get("longitude"), - "latitude": round(lat_val, 6) if lat_val is not None else vehicle.get("latitude") - } - unmanned_vehicles.append(location_info) - - return unmanned_vehicles # 生成符合API规范的完整车辆状态数据 def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]: @@ -574,11 +573,102 @@ def generate_comprehensive_vehicle_status(vehicle_id: str) -> dict[str, Any]: "cellularSignalStrength": -65, # -65dBm "wifiStatus": "CONNECTED", "cloudConnectivity": "ONLINE" + }, + "missionContext": { + "currentMission": { + "missionId": vehicle_data_item.get("mission_id", "UNKNOWN"), + "missionType": vehicle_data_item.get("mission_type", "UNKNOWN"), + "startTime": vehicle_data_item.get("mission_start_time", timestamp_ms), + "estimatedEndTime": vehicle_data_item.get("mission_start_time", timestamp_ms) + 3600000, # 1小时后 + "progress": round(calculate_mission_progress(vehicle_data_item), 1), + "totalMileage": round(to_float_safe(vehicle_data_item.get("total_mileage", 0.0)) or 0.0, 1) + }, + "waypoints": [ + { + "waypointId": f"{vehicle_id}_START", + "latitude": round(to_float_safe(vehicle_data_item.get("start_point", {}).get("latitude", 0.0)) or 0.0, 6), + "longitude": round(to_float_safe(vehicle_data_item.get("start_point", {}).get("longitude", 0.0)) or 0.0, 6), + "status": "COMPLETED" if not vehicle_data_item.get("moving_to_end", True) else "PENDING" + }, + { + "waypointId": f"{vehicle_id}_END", + "latitude": round(to_float_safe(vehicle_data_item.get("end_point", {}).get("latitude", 0.0)) or 0.0, 6), + "longitude": round(to_float_safe(vehicle_data_item.get("end_point", {}).get("longitude", 0.0)) or 0.0, 6), + "status": "COMPLETED" if vehicle_data_item.get("moving_to_end", True) else "PENDING" + } + ] } } return status_data +def calculate_mission_progress(vehicle_data: dict[str, Any]) -> float: + """计算任务进度百分比""" + try: + # 获取车辆当前位置 + cur_lat = to_float_safe(vehicle_data.get("latitude")) + cur_lon = to_float_safe(vehicle_data.get("longitude")) + + # 获取起点和终点坐标 + start_point = vehicle_data.get("start_point", {}) + end_point = vehicle_data.get("end_point", {}) + + start_lat = to_float_safe(start_point.get("latitude")) + start_lon = to_float_safe(start_point.get("longitude")) + end_lat = to_float_safe(end_point.get("latitude")) + end_lon = to_float_safe(end_point.get("longitude")) + + if any(v is None for v in [cur_lat, cur_lon, start_lat, start_lon, end_lat, end_lon]): + return 0.0 + + # 确保所有值都不为None,使用断言来帮助类型检查器理解 + assert cur_lat is not None and cur_lon is not None + assert start_lat is not None and start_lon is not None + assert end_lat is not None and end_lon is not None + + # 现在可以安全地转换为float类型 + start_lat_safe = float(start_lat) + start_lon_safe = float(start_lon) + end_lat_safe = float(end_lat) + end_lon_safe = float(end_lon) + cur_lat_safe = float(cur_lat) + cur_lon_safe = float(cur_lon) + + # 计算起点到终点的总距离 + total_distance = calculate_distance_to_target( + {"latitude": start_lat_safe, "longitude": start_lon_safe}, + end_lat_safe, end_lon_safe + ) + + if total_distance == 0: + return 100.0 + + # 计算当前位置到起点的距离 + distance_from_start = calculate_distance_to_target( + {"latitude": cur_lat_safe, "longitude": cur_lon_safe}, + start_lat_safe, start_lon_safe + ) + + # 计算当前位置到终点的距离 + distance_to_end = calculate_distance_to_target( + {"latitude": cur_lat_safe, "longitude": cur_lon_safe}, + end_lat_safe, end_lon_safe + ) + + # 根据移动方向计算进度 + if vehicle_data.get("moving_to_end", True): + # 向终点移动:进度 = (已走距离 / 总距离) * 100 + progress = (distance_from_start / total_distance) * 100.0 + else: + # 向起点移动:进度 = (1 - 距离起点的距离 / 总距离) * 100 + 100 + progress = 100.0 + ((total_distance - distance_from_start) / total_distance) * 100.0 + + # 确保进度在合理范围内 + return max(0.0, min(200.0, progress)) + + except Exception: + return 0.0 + def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]: """根据fields参数过滤车辆状态字段""" if not fields: @@ -600,59 +690,6 @@ def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None return filtered_data -# 无人车状态数据生成函数 -def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]: - """生成无人车状态数据,符合官方API格式""" - vehicle_states_data = [] - - if is_single and vehicle_id: - # 单个车辆状态查询 - vehicle_state = vehicle_states.get(vehicle_id) - if vehicle_state: - state_info = { - "transId": str(uuid.uuid4()), - "timestamp": int(time.time() * 1000), - "vehicleID": vehicle_id, - "loginState": True, - "faultInfo": [], - "activeSafety": not vehicle_state.is_running, - "RC": False, - "Command": 1 if vehicle_state.current_command == "ALERT" else 0, - "airportInfo": [], - "vehicleMode": 2, # 自动模式 - "gearState": 2, # D档 - "chassisReady": vehicle_state.is_running, - "collisionStatus": False, - "clearance": 1 if vehicle_state.is_running else 0, - "turnSignalStstus": 0, - "pointCloud": [] - } - vehicle_states_data.append(state_info) - else: - # 所有车辆状态查询 - for vehicle in unmanned_vehicle_data: - vehicle_state = vehicle_states.get(vehicle["vehicleNo"]) - state_info = { - "transId": str(uuid.uuid4()), - "timestamp": int(time.time() * 1000), - "vehicleID": vehicle["vehicleNo"], - "loginState": True, - "faultInfo": [], - "activeSafety": not vehicle_state.is_running if vehicle_state else False, - "RC": False, - "Command": 1 if vehicle_state and vehicle_state.current_command == "ALERT" else 0, - "airportInfo": [], - "vehicleMode": 2, - "gearState": 2, - "chassisReady": vehicle_state.is_running if vehicle_state else True, - "collisionStatus": False, - "clearance": 1 if vehicle_state and vehicle_state.is_running else 0, - "turnSignalStstus": 0, - "pointCloud": [] - } - vehicle_states_data.append(state_info) - - return vehicle_states_data # API 端点实现 @@ -805,51 +842,7 @@ def handle_vehicle_command(): "timestamp": int(time.time() * 1000) }), 500 -@app.route('/api/VehicleLocationInfo', methods=['GET', 'OPTIONS']) -def get_unmanned_vehicle_location(): - """无人车位置上报接口""" - if request.method == 'OPTIONS': - return '', 204 - try: - current_time = time.time() - global last_unmanned_vehicle_update_time - elapsed_time = current_time - last_unmanned_vehicle_update_time - if elapsed_time >= UPDATE_INTERVAL: - for vehicle in unmanned_vehicle_data: - update_vehicle_position(vehicle, UPDATE_INTERVAL) - vehicle["time"] = int(current_time * 1000) - last_unmanned_vehicle_update_time = current_time - # 生成无人车位置数据 - location_data = generate_unmanned_vehicle_location_data() - logging.info(f"📍 返回无人车位置数据,车辆数量: {len(location_data)}") - return jsonify(location_data) - except Exception as e: - logging.error(f"Error in get_unmanned_vehicle_location: {str(e)}") - return jsonify([]), 500 - -@app.route('/api/VehicleStateInfo', methods=['POST', 'OPTIONS']) -def get_unmanned_vehicle_state(): - """无人车状态查询接口""" - if request.method == 'OPTIONS': - return '', 204 - - try: - data = request.json - vehicle_id = data.get("vehicleID") if data else None - is_single = data.get("isSingle", True) if data else False - - logging.info(f"📊 收到无人车状态查询请求: vehicle_id={vehicle_id}, is_single={is_single}") - - # 生成无人车状态数据 - state_data = generate_unmanned_vehicle_state_data(vehicle_id, is_single) - - logging.info(f"返回无人车状态数据,数量: {len(state_data)}") - return jsonify(state_data) - - except Exception as e: - logging.error(f"Error in get_unmanned_vehicle_state: {str(e)}") - return jsonify([]), 500 @app.route('/api/v1/vehicles//status', methods=['GET', 'OPTIONS']) def get_vehicle_status_universal(vehicle_id): @@ -944,43 +937,6 @@ def get_vehicle_status_universal(vehicle_id): } }), 500 -@app.route('/openApi/getVehicleStatus', methods=['GET', 'OPTIONS']) -def get_vehicle_status(): - """获取无人车状态信息""" - if request.method == 'OPTIONS': - return '', 204 - - vehicle_id = request.args.get('vehicleId') - if not vehicle_id: - return jsonify({ - "status": 400, - "msg": "缺少 vehicleId 参数", - "data": None - }), 400 - - # 查找对应的车辆 - vehicle_state = vehicle_states.get(vehicle_id) - if not vehicle_state: - return jsonify({ - "status": 404, - "msg": f"未找到无人车 {vehicle_id}", - "data": None - }), 404 - - # 返回车辆状态 - logging.info(f"📋 返回无人车状态: vehicleId={vehicle_id}") - return jsonify({ - "status": 200, - "msg": "获取无人车状态成功", - "data": { - "vehicleId": vehicle_id, - "status": "NORMAL" if vehicle_state.is_running else "STOPPED", - "command": vehicle_state.current_command, - "commandPriority": vehicle_state.command_priority, - "trafficLightState": vehicle_state.traffic_light_state, - "timestamp": int(time.time() * 1000) - } - }) # 设置CORS @app.after_request @@ -995,8 +951,6 @@ if __name__ == '__main__': print("🚗 无人车厂商平台模拟服务启动中...") print("📍 服务端点:") print(" - 车辆控制指令: POST /api/VehicleCommandInfo") - print(" - 车辆位置上报: GET /api/VehicleLocationInfo") - print(" - 车辆状态查询: POST /api/VehicleStateInfo") print(" - 通用状态接口: GET /api/v1/vehicles//status") print(" - 车辆状态获取: GET /openApi/getVehicleStatus") print(f"🌐 服务地址: http://localhost:8091") diff --git a/tools/test_websocket.html b/tools/test_websocket.html index b7dde42f..e62bde2d 100644 --- a/tools/test_websocket.html +++ b/tools/test_websocket.html @@ -121,7 +121,6 @@
路口红绿灯: 0
航空器路由: 0
事件驱动路由: 0
-
定时采集路由: 0
航班进出港: 0
路径冲突: 0
车辆指令: 0
@@ -189,7 +188,6 @@ intersection_traffic_light_status: 0, aircraftRouteUpdate: 0, eventDrivenRoute: 0, // 事件驱动的路由更新 - scheduledRoute: 0, // 定时采集的路由更新 flight_notification: 0, path_conflict_alert: 0, vehicle_command: 0, @@ -257,7 +255,6 @@ document.getElementById('intersectionTrafficLightCount').textContent = messageStats.intersection_traffic_light_status; document.getElementById('aircraftRouteCount').textContent = messageStats.aircraftRouteUpdate; document.getElementById('eventDrivenRouteCount').textContent = messageStats.eventDrivenRoute; - document.getElementById('scheduledRouteCount').textContent = messageStats.scheduledRoute; document.getElementById('flightNotificationCount').textContent = messageStats.flight_notification; document.getElementById('pathConflictCount').textContent = messageStats.path_conflict_alert; document.getElementById('vehicleCommandCount').textContent = messageStats.vehicle_command; @@ -421,8 +418,6 @@ // 根据事件来源更新分类统计 if (eventSource === 'FLIGHT_NOTIFICATION') { messageStats.eventDrivenRoute++; - } else if (eventSource === 'SCHEDULED') { - messageStats.scheduledRoute++; } // 根据事件来源设置不同的日志类型和图标 @@ -431,8 +426,6 @@ if (eventSource === 'FLIGHT_NOTIFICATION') { logType = 'success'; icon = '🚀'; // 事件驱动的路由更新用火箭图标 - } else if (eventSource === 'SCHEDULED') { - icon = '🕐'; // 定时采集的路由更新用时钟图标 } log('collisionLog', `${icon} 航空器路由更新: ${flightNo} | 路由:${routeType} 状态:${routeStatus} | 航空器:${aircraftStatus} | 编码:${routeCodes} | 来源:${eventSource} | ${routeGeometry}`, logType); @@ -598,7 +591,6 @@ document.getElementById('intersectionTrafficLightCount').textContent = '0'; document.getElementById('aircraftRouteCount').textContent = '0'; document.getElementById('eventDrivenRouteCount').textContent = '0'; - document.getElementById('scheduledRouteCount').textContent = '0'; document.getElementById('flightNotificationCount').textContent = '0'; document.getElementById('pathConflictCount').textContent = '0'; document.getElementById('vehicleCommandCount').textContent = '0';