修改航空器路由完整机制
This commit is contained in:
parent
589a212877
commit
7556a5e3c3
196
doc/work/红绿灯设备固定标识识别方案_20250119.md
Normal file
196
doc/work/红绿灯设备固定标识识别方案_20250119.md
Normal file
@ -0,0 +1,196 @@
|
||||
# 红绿灯设备固定标识识别方案
|
||||
|
||||
**文档日期**: 2025年1月19日
|
||||
**创建人**: AI Assistant
|
||||
**项目**: QAUP-Management 机场冲突避让管理系统
|
||||
|
||||
## 问题背景
|
||||
|
||||
当前系统计划接入多台红绿灯设备(物联网设备),面临以下挑战:
|
||||
|
||||
1. **设备识别困难**: 接收到的信息格式固定,无法通过消息来区分是哪个红绿灯
|
||||
2. **网络地址不稳定**: 物联网设备通过蜂窝网络接入,IP地址每次重启都可能变化
|
||||
3. **现有识别方式局限**: 系统目前仅使用IP地址作为设备识别,不适用于动态IP场景
|
||||
|
||||
## 技术调研结果
|
||||
|
||||
### 国标协议支持
|
||||
- **GB/T 20999-2017**: 交通信号控制机与上位机间的数据通信协议
|
||||
- **GB/T 43229-2023**: 交通信号控制机与车辆检测器间通信协议
|
||||
- **GA/T 1049.2-2024**: 公安交通集成指挥平台通信协议
|
||||
|
||||
### 设备标识技术可行性
|
||||
根据网络调研,物联网红绿灯设备**完全支持**在消息中发送设备ID:
|
||||
|
||||
✅ **设备ID (Device ID)**: IoT平台普遍要求设备包含厂商ID、终端型号、终端ID信息
|
||||
✅ **序列号 (Serial Number)**: 设备硬件序列号,全球唯一
|
||||
✅ **MAC地址**: 48位网络地址,前3字节为OUI组织唯一标识符
|
||||
✅ **JSON格式支持**: 现代IoT设备普遍使用JSON格式,天然支持标识字段
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案1:要求设备厂商在消息中添加设备标识(推荐)
|
||||
|
||||
**技术依据**:
|
||||
- GB/T 20999-2017国标协议支持设备标识
|
||||
- IoT设备普遍使用JSON格式,天然支持device_id字段
|
||||
- 行业最佳实践要求设备身份识别
|
||||
|
||||
**消息格式扩展**:
|
||||
```json
|
||||
{
|
||||
"device_id": "TL001_ABCD1234", // 设备唯一标识
|
||||
"serial_number": "202401001234", // 设备序列号
|
||||
"mac_address": "00:1A:2B:3C:4D:5E", // 网络MAC地址
|
||||
"timestamp": 1642492800000,
|
||||
"DI-01": "1", // 原有DI信号数据
|
||||
"DI-02": "0"
|
||||
}
|
||||
```
|
||||
|
||||
**实施步骤**:
|
||||
1. 与设备厂商沟通,要求在现有DI信号消息中添加设备标识字段
|
||||
2. 修改TrafficLightSignalHandler解析逻辑,支持设备ID提取
|
||||
3. 扩展设备管理数据库,存储设备标识映射关系
|
||||
4. 实现基于设备ID的智能识别和管理
|
||||
|
||||
### 方案2:TCP连接层面获取网络标识(备用方案)
|
||||
|
||||
**适用场景**: 设备厂商暂时无法修改消息格式时的临时方案
|
||||
|
||||
**技术实现**:
|
||||
- 扩展ClientConnection类,尝试获取网络接口信息
|
||||
- 实现连接指纹识别机制
|
||||
- 建立IP地址与设备特征的映射关系
|
||||
|
||||
**局限性**:
|
||||
- 可靠性低于应用层标识
|
||||
- 无法获取真正的硬件唯一标识
|
||||
- 依赖网络层特征,可能不稳定
|
||||
|
||||
### 方案3:混合识别策略(最佳实践)
|
||||
|
||||
**多重标识优先级**:
|
||||
1. **主标识**: 消息中的device_id/serial_number(最可靠)
|
||||
2. **辅助标识**: TCP连接特征指纹
|
||||
3. **备用标识**: IP地址(现有方式)
|
||||
|
||||
**智能匹配逻辑**:
|
||||
- **设备迁移检测**: 同一device_id出现新IP时自动更新映射
|
||||
- **冲突处理**: 同一IP出现不同device_id时记录告警
|
||||
- **历史记录**: 维护设备标识变更历史,便于故障排查
|
||||
|
||||
## 代码实现要点
|
||||
|
||||
### 1. 消息解析器扩展
|
||||
```java
|
||||
// TrafficLightSignalParser增强
|
||||
public class TrafficLightSignalParser {
|
||||
public ParsedSignal parseSignalWithDeviceId(String jsonMessage) {
|
||||
// 解析设备标识字段
|
||||
String deviceId = extractDeviceId(jsonMessage);
|
||||
String serialNumber = extractSerialNumber(jsonMessage);
|
||||
String macAddress = extractMacAddress(jsonMessage);
|
||||
|
||||
// 创建包含设备标识的解析结果
|
||||
return ParsedSignal.builder()
|
||||
.deviceId(deviceId)
|
||||
.serialNumber(serialNumber)
|
||||
.macAddress(macAddress)
|
||||
.signalData(extractSignalData(jsonMessage))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 设备管理服务增强
|
||||
```java
|
||||
// TrafficLightService扩展
|
||||
public class TrafficLightService {
|
||||
public TrafficLightDevice findDeviceByIdentifier(String deviceId, String serialNumber, String ipAddress) {
|
||||
// 优先级查找:设备ID > 序列号 > IP地址
|
||||
TrafficLightDevice device = findByDeviceId(deviceId);
|
||||
if (device == null) {
|
||||
device = findBySerialNumber(serialNumber);
|
||||
}
|
||||
if (device == null) {
|
||||
device = findByIpAddress(ipAddress);
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
public void updateDeviceMapping(String deviceId, String ipAddress) {
|
||||
// 更新设备ID与IP地址的映射关系
|
||||
// 检测设备迁移,记录变更历史
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 数据库模型扩展
|
||||
```sql
|
||||
-- 扩展红绿灯设备表
|
||||
ALTER TABLE traffic_light_devices ADD COLUMN device_id VARCHAR(100);
|
||||
ALTER TABLE traffic_light_devices ADD COLUMN serial_number VARCHAR(100);
|
||||
ALTER TABLE traffic_light_devices ADD COLUMN mac_address VARCHAR(17);
|
||||
ALTER TABLE traffic_light_devices ADD COLUMN last_ip_address VARCHAR(15);
|
||||
ALTER TABLE traffic_light_devices ADD COLUMN ip_change_history TEXT;
|
||||
|
||||
-- 创建设备标识索引
|
||||
CREATE INDEX idx_device_id ON traffic_light_devices(device_id);
|
||||
CREATE INDEX idx_serial_number ON traffic_light_devices(serial_number);
|
||||
CREATE INDEX idx_mac_address ON traffic_light_devices(mac_address);
|
||||
```
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 第一阶段:协调与准备(1-2周)
|
||||
- [ ] 联系设备厂商,讨论消息格式扩展需求
|
||||
- [ ] 确认设备标识字段的具体格式和命名规范
|
||||
- [ ] 制定设备标识管理策略和数据库设计
|
||||
|
||||
### 第二阶段:代码实现(2-3周)
|
||||
- [ ] 扩展消息解析器,支持设备标识提取
|
||||
- [ ] 修改设备管理服务,实现多重标识查找
|
||||
- [ ] 扩展数据库模型,存储设备标识信息
|
||||
- [ ] 实现设备迁移检测和冲突处理逻辑
|
||||
|
||||
### 第三阶段:测试与部署(1-2周)
|
||||
- [ ] 单元测试和集成测试
|
||||
- [ ] 设备标识功能验证
|
||||
- [ ] 设备迁移场景测试
|
||||
- [ ] 生产环境部署和监控
|
||||
|
||||
### 第四阶段:运维优化(持续)
|
||||
- [ ] 监控设备标识识别效果
|
||||
- [ ] 优化匹配算法和冲突处理
|
||||
- [ ] 建立设备生命周期管理流程
|
||||
|
||||
## 预期效果
|
||||
|
||||
### 直接效益
|
||||
- ✅ **彻底解决IP地址变化问题**: 设备重启后仍能准确识别
|
||||
- ✅ **提供可靠的设备唯一标识**: 基于硬件序列号的全球唯一性
|
||||
- ✅ **符合国标协议要求**: 遵循GB/T 20999-2017等标准规范
|
||||
- ✅ **支持设备精准管理**: 实现设备的全生命周期追踪
|
||||
|
||||
### 间接效益
|
||||
- 提升系统可靠性和稳定性
|
||||
- 降低设备管理和运维成本
|
||||
- 为后续设备扩展提供标准化基础
|
||||
- 增强故障排查和问题定位能力
|
||||
|
||||
## 风险评估与应对
|
||||
|
||||
### 主要风险
|
||||
1. **设备厂商配合度**: 可能存在厂商不愿意修改消息格式的情况
|
||||
2. **消息格式兼容性**: 新旧设备消息格式可能存在差异
|
||||
3. **性能影响**: 额外的设备标识处理可能影响系统性能
|
||||
|
||||
### 应对措施
|
||||
1. **多方案准备**: 同时准备方案1和方案2,确保有备选方案
|
||||
2. **向后兼容**: 设计时考虑向后兼容,支持新旧消息格式共存
|
||||
3. **性能优化**: 实现高效的设备标识缓存和查找机制
|
||||
|
||||
## 总结
|
||||
|
||||
基于技术调研结果,**强烈建议采用方案1**(要求设备厂商添加设备标识),这是最符合行业标准和最佳实践的解决方案。通过在DI信号消息中添加device_id、serial_number等标识字段,可以彻底解决物联网设备动态IP地址带来的识别问题,为系统的稳定运行和后续扩展奠定坚实基础。
|
||||
@ -225,7 +225,7 @@ data:
|
||||
password: dianxin@123
|
||||
# 无人车厂商数据源配置
|
||||
vehicle-api:
|
||||
base-url: http://localhost:8090
|
||||
base-url: http://localhost:8091
|
||||
endpoints:
|
||||
vehicle-location: /api/VehicleLocationInfo
|
||||
vehicle-state: /api/VehicleStateInfo
|
||||
|
||||
@ -9,6 +9,7 @@ import com.qaup.collision.common.model.dto.Response;
|
||||
import com.qaup.collision.datacollector.service.AuthService;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
|
||||
import com.qaup.collision.datacollector.dto.AircraftStatusDTO;
|
||||
import com.qaup.collision.datacollector.dto.AircraftRouteParamsDTO;
|
||||
import com.qaup.collision.datacollector.dto.FlightNotificationDTO;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
@ -44,6 +45,10 @@ import java.util.stream.Collectors;
|
||||
@Component
|
||||
public class DataCollectorDao {
|
||||
|
||||
// 机场数据源相关配置
|
||||
@Value("${data.collector.airport-api.base-url}")
|
||||
private String airportBaseUrl;
|
||||
|
||||
// 无人车厂商数据源相关配置
|
||||
@Value("${data.collector.vehicle-api.base-url}")
|
||||
private String vehicleBaseUrl;
|
||||
@ -272,7 +277,7 @@ public class DataCollectorDao {
|
||||
|
||||
/**
|
||||
* 获取航空器进港路由
|
||||
*
|
||||
*
|
||||
* @param inRunway 进港跑道编号
|
||||
* @param outRunway 出港跑道编号
|
||||
* @param contactCross 联络道口
|
||||
@ -287,7 +292,7 @@ public class DataCollectorDao {
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = UriComponentsBuilder.fromUriString(vehicleBaseUrl)
|
||||
String url = UriComponentsBuilder.fromUriString(airportBaseUrl)
|
||||
.path("/runwayPathPlanningController/findArrTaxiwayByRunwayAndContactCrossAndSeat")
|
||||
.queryParam("inRunway", inRunway)
|
||||
.queryParam("outRunway", outRunway)
|
||||
@ -326,7 +331,7 @@ public class DataCollectorDao {
|
||||
|
||||
/**
|
||||
* 获取航空器出港路由
|
||||
*
|
||||
*
|
||||
* @param inRunway 进港跑道编号
|
||||
* @param outRunway 出港跑道编号
|
||||
* @param startSeat 起始机位
|
||||
@ -340,7 +345,7 @@ public class DataCollectorDao {
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = UriComponentsBuilder.fromUriString(vehicleBaseUrl)
|
||||
String url = UriComponentsBuilder.fromUriString(airportBaseUrl)
|
||||
.path("/runwayPathPlanningController/findDepTaxiwayByRunwayAndContactCrossAndSeat")
|
||||
.queryParam("inRunway", inRunway)
|
||||
.queryParam("outRunway", outRunway)
|
||||
@ -389,7 +394,7 @@ public class DataCollectorDao {
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = vehicleBaseUrl + "/aircraftStatusController/getAircraftStatus";
|
||||
String url = airportBaseUrl + "/aircraftStatusController/getAircraftStatus";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", token);
|
||||
@ -418,6 +423,57 @@ public class DataCollectorDao {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取航班路由参数
|
||||
*
|
||||
* 数据来源:机场系统API (/aircraftRouteParamsController/getRouteParams)
|
||||
* 说明:根据航班号和路由类型获取完整的路由查询参数
|
||||
*
|
||||
* @param flightNo 航班号
|
||||
* @param routeType 路由类型(IN或OUT)
|
||||
* @return 航班路由参数数据
|
||||
*/
|
||||
public AircraftRouteParamsDTO getAircraftRouteParams(String flightNo, String routeType) {
|
||||
try {
|
||||
String token = authService.getToken();
|
||||
if (token == null) {
|
||||
log.error("无法获取有效的认证token");
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = UriComponentsBuilder.fromUriString(airportBaseUrl)
|
||||
.path("/aircraftRouteParamsController/getRouteParams")
|
||||
.queryParam("flightNo", flightNo)
|
||||
.queryParam("routeType", routeType)
|
||||
.toUriString();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", token);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<Response<AircraftRouteParamsDTO>> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
requestEntity,
|
||||
new ParameterizedTypeReference<Response<AircraftRouteParamsDTO>>() {}
|
||||
);
|
||||
|
||||
Response<AircraftRouteParamsDTO> responseBody = response.getBody();
|
||||
if (responseBody != null && responseBody.getStatus() == 200) {
|
||||
log.info("成功获取航班路由参数: flightNo={}, routeType={}", flightNo, routeType);
|
||||
return responseBody.getData();
|
||||
} else {
|
||||
log.warn("获取航班路由参数失败: {}", responseBody != null ? responseBody.getMsg() : "未知错误");
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取航班路由参数时发生异常: flightNo={}, routeType={}", flightNo, routeType, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取航班进出港通知
|
||||
*
|
||||
|
||||
@ -0,0 +1,92 @@
|
||||
package com.qaup.collision.datacollector.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* 航班路由参数DTO
|
||||
*
|
||||
* 数据来源:机场系统API (/aircraftRouteParamsController/getRouteParams)
|
||||
* 说明:根据航班号和路由类型获取的完整路由查询参数
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AircraftRouteParamsDTO {
|
||||
|
||||
/**
|
||||
* 航班号
|
||||
*/
|
||||
@JsonProperty("flightNo")
|
||||
private String flightNo;
|
||||
|
||||
/**
|
||||
* 路由类型(IN:进港,OUT:出港)
|
||||
*/
|
||||
@JsonProperty("routeType")
|
||||
private String routeType;
|
||||
|
||||
/**
|
||||
* 进港跑道编号
|
||||
*/
|
||||
@JsonProperty("inRunway")
|
||||
private String inRunway;
|
||||
|
||||
/**
|
||||
* 出港跑道编号
|
||||
*/
|
||||
@JsonProperty("outRunway")
|
||||
private String outRunway;
|
||||
|
||||
/**
|
||||
* 联络道口(进港时使用)
|
||||
*/
|
||||
@JsonProperty("contactCross")
|
||||
private String contactCross;
|
||||
|
||||
/**
|
||||
* 目的机位(进港时使用)
|
||||
*/
|
||||
@JsonProperty("seat")
|
||||
private String seat;
|
||||
|
||||
/**
|
||||
* 起始机位(出港时使用)
|
||||
*/
|
||||
@JsonProperty("startSeat")
|
||||
private String startSeat;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
@JsonProperty("timestamp")
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 检查是否为有效的路由参数
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return flightNo != null && !flightNo.trim().isEmpty() &&
|
||||
routeType != null && !routeType.trim().isEmpty() &&
|
||||
inRunway != null && !inRunway.trim().isEmpty() &&
|
||||
outRunway != null && !outRunway.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为进港路由参数
|
||||
*/
|
||||
public boolean isArrivalRoute() {
|
||||
return "IN".equalsIgnoreCase(routeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为出港路由参数
|
||||
*/
|
||||
public boolean isDepartureRoute() {
|
||||
return "OUT".equalsIgnoreCase(routeType);
|
||||
}
|
||||
}
|
||||
@ -357,6 +357,7 @@ public class DataCollectorService {
|
||||
.aircraftStatus(status.getType())
|
||||
.routeGeometry(route.getGeometry() != null ? route.getGeometry().toText() : null)
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.eventSource("SCHEDULED") // 标识为定时采集触发的事件
|
||||
.build();
|
||||
|
||||
// 发布WebSocket事件
|
||||
@ -833,6 +834,10 @@ public class DataCollectorService {
|
||||
// 发布WebSocket事件推送到前端
|
||||
publishFlightNotificationEvent(notification);
|
||||
|
||||
// 🚀 新增:事件驱动的路由查询
|
||||
// 基于航班通知触发路由查询和更新
|
||||
triggerRouteQueryByFlightNotification(dto);
|
||||
|
||||
} else {
|
||||
log.warn("⚠️ 航班进出港通知数据无效,跳过处理: {}", dto);
|
||||
}
|
||||
@ -915,6 +920,155 @@ public class DataCollectorService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于航班通知触发路由查询
|
||||
*
|
||||
* 实现事件驱动的路由更新流程:
|
||||
* 1. 收到航班进出港通知
|
||||
* 2. 使用航班号和类型查询路由参数
|
||||
* 3. 调用路由查询接口获取完整路由数据
|
||||
* 4. 发布WebSocket路由更新事件
|
||||
*
|
||||
* @param flightNotification 航班进出港通知
|
||||
*/
|
||||
private void triggerRouteQueryByFlightNotification(FlightNotificationDTO flightNotification) {
|
||||
try {
|
||||
String flightNo = flightNotification.getFlightNo();
|
||||
String routeType = flightNotification.getType();
|
||||
|
||||
log.info("🛫 航班通知触发路由查询: 航班号={}, 类型={}", flightNo, routeType);
|
||||
|
||||
// 步骤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);
|
||||
|
||||
log.info("🚀 事件驱动的路由更新完成: 航班号={}, 路由类型={}", flightNo, routeType);
|
||||
} 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() {
|
||||
log.info("DataCollectorService 正在关闭...");
|
||||
|
||||
@ -54,4 +54,9 @@ public class AircraftRouteUpdateEvent {
|
||||
* 事件时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 事件来源 (SCHEDULED: 定时采集, FLIGHT_NOTIFICATION: 航班通知触发)
|
||||
*/
|
||||
private String eventSource;
|
||||
}
|
||||
@ -5,7 +5,6 @@ import com.qaup.collision.websocket.event.FlightNotificationEvent;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
@ -174,9 +173,6 @@ class FlightNotificationIntegrationTest {
|
||||
void testEmptyDataHandling() {
|
||||
System.out.println("=== 测试空数据处理 ===");
|
||||
|
||||
// 记录事件发布前的调用次数
|
||||
int initialEventCount = mockingDetails(eventPublisher).getInvocations().size();
|
||||
|
||||
// 执行数据采集(可能返回空数据)
|
||||
dataCollectorService.collectFlightNotificationData();
|
||||
|
||||
|
||||
2722
tools/mock_server-copy_20250919.py
Normal file
2722
tools/mock_server-copy_20250919.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -96,13 +96,6 @@ SPECIAL_VEHICLE_END = {"longitude": 120.083899, "latitude": 36.367403} # 特
|
||||
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
|
||||
@ -137,12 +130,12 @@ flight_data = {
|
||||
}
|
||||
}
|
||||
|
||||
# CA3456 航空器路由参数配置
|
||||
# 航空器路由参数配置
|
||||
aircraft_route_params = {
|
||||
"CA3456": {
|
||||
"arrival": { # 进港参数
|
||||
"inRunway": "35",
|
||||
"outRunway": "34",
|
||||
"outRunway": "34",
|
||||
"contactCross": "F1",
|
||||
"seat": "138"
|
||||
},
|
||||
@ -156,7 +149,7 @@ aircraft_route_params = {
|
||||
"arrival": {
|
||||
"inRunway": "17",
|
||||
"outRunway": "35",
|
||||
"contactCross": "A2",
|
||||
"contactCross": "A2",
|
||||
"seat": "201"
|
||||
},
|
||||
"departure": {
|
||||
@ -174,7 +167,34 @@ aircraft_route_params = {
|
||||
},
|
||||
"departure": {
|
||||
"inRunway": "35",
|
||||
"outRunway": "17",
|
||||
"outRunway": "17",
|
||||
"startSeat": "156"
|
||||
}
|
||||
},
|
||||
# 新增:航班进出港通知中出现的航班配置
|
||||
"CA8901": { # 出港航班
|
||||
"arrival": {
|
||||
"inRunway": "35",
|
||||
"outRunway": "17",
|
||||
"contactCross": "L4",
|
||||
"seat": "201"
|
||||
},
|
||||
"departure": { # 出港参数
|
||||
"inRunway": "35",
|
||||
"outRunway": "17",
|
||||
"startSeat": "201"
|
||||
}
|
||||
},
|
||||
"MU2678": { # 进港航班
|
||||
"arrival": { # 进港参数
|
||||
"inRunway": "17",
|
||||
"outRunway": "35",
|
||||
"contactCross": "B3",
|
||||
"seat": "156"
|
||||
},
|
||||
"departure": {
|
||||
"inRunway": "17",
|
||||
"outRunway": "35",
|
||||
"startSeat": "156"
|
||||
}
|
||||
}
|
||||
@ -1282,34 +1302,11 @@ vehicle_data = [
|
||||
"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
|
||||
@ -1441,7 +1438,7 @@ class VehicleState:
|
||||
|
||||
# 添加车辆状态管理
|
||||
vehicle_states = {}
|
||||
for vehicle in vehicle_data:
|
||||
for vehicle in airport_vehicle_data:
|
||||
vehicle_states[str(vehicle["vehicleNo"])] = VehicleState(str(vehicle["vehicleNo"]))
|
||||
|
||||
from collections.abc import Mapping
|
||||
@ -1464,187 +1461,7 @@ def calculate_distance_to_target(vehicle: Mapping[str, float], target_lat: float
|
||||
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
|
||||
|
||||
# 删除了复杂的红绿灯和路口计算函数,因为已简化为往复运动逻辑
|
||||
|
||||
@ -1840,10 +1657,9 @@ def generate_traffic_light_data() -> list[dict[str, Any]]:
|
||||
# 红绿灯数据
|
||||
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:
|
||||
@ -2105,270 +1921,11 @@ def login():
|
||||
"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 None
|
||||
|
||||
# 获取车辆状态
|
||||
vehicle_state = vehicle_states.get(vehicle_id)
|
||||
|
||||
# 基础位置和运动数据
|
||||
lat_val = to_float_safe(vehicle_data_item.get("latitude"))
|
||||
lon_val = to_float_safe(vehicle_data_item.get("longitude"))
|
||||
speed_val = to_float_safe(vehicle_data_item.get("speed", 0))
|
||||
direction_val = to_float_safe(vehicle_data_item.get("direction", 0))
|
||||
|
||||
# 生成完整状态数据
|
||||
status_data = {
|
||||
"vehicleInfo": {
|
||||
"vehicleId": vehicle_id
|
||||
},
|
||||
"operationalStatus": {
|
||||
"powerStatus": "ON",
|
||||
"systemHealth": "HEALTHY" if vehicle_state and vehicle_state.is_running else "DEGRADED",
|
||||
"operationalMode": "AUTONOMOUS",
|
||||
"emergencyStatus": "CRITICAL" if vehicle_state and vehicle_state.current_command == "ALERT"
|
||||
else "WARNING" if vehicle_state and vehicle_state.current_command == "WARNING"
|
||||
else "NORMAL",
|
||||
"lastHeartbeat": timestamp_ms
|
||||
},
|
||||
"controlStatus": {
|
||||
"controlMode": "AUTONOMOUS",
|
||||
"controlAuthority": "SYSTEM",
|
||||
"remoteControlActive": vehicle_state.remoteControlActive if vehicle_state else False
|
||||
},
|
||||
"motionStatus": {
|
||||
"position": {
|
||||
"latitude": round(lat_val, 6) if lat_val is not None else 0.0,
|
||||
"longitude": round(lon_val, 6) if lon_val is not None else 0.0
|
||||
},
|
||||
"velocity": {
|
||||
"speed": round(speed_val * 1000 / 3600, 2) if speed_val is not None else 0.0, # 转换为 m/s
|
||||
"direction": round(math.radians(direction_val), 4) if direction_val is not None else 0.0 # 转换为弧度
|
||||
}
|
||||
},
|
||||
"safetyStatus": {
|
||||
"collisionAvoidanceActive": vehicle_state.is_running if vehicle_state else True,
|
||||
"emergencyBrakingReady": True,
|
||||
"pathPlanningStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE",
|
||||
"obstacleDetectionStatus": "ACTIVE" if vehicle_state and vehicle_state.is_running else "INACTIVE",
|
||||
"minimumRiskManeuverTriggered": vehicle_state.current_command == "ALERT" if vehicle_state else False
|
||||
},
|
||||
"sensorStatus": {
|
||||
"gps": {
|
||||
"status": "ACTIVE",
|
||||
"accuracy": 0.5, # 0.5米精度
|
||||
"lastUpdate": timestamp_ms
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"mainBattery": {
|
||||
"chargeLevel": 85.5, # 85.5%
|
||||
"voltage": 48.2, # 48.2V
|
||||
"current": -15.3, # -15.3A (放电)
|
||||
"temperature": 35.2, # 35.2°C
|
||||
"chargingStatus": "DISCHARGING"
|
||||
}
|
||||
},
|
||||
"communicationStatus": {
|
||||
"v2xStatus": "CONNECTED",
|
||||
"cellularSignalStrength": -65, # -65dBm
|
||||
"wifiStatus": "CONNECTED",
|
||||
"cloudConnectivity": "ONLINE"
|
||||
}
|
||||
}
|
||||
|
||||
return status_data
|
||||
|
||||
def filter_vehicle_status_fields(status_data: dict[str, Any], fields: str | None) -> dict[str, Any]:
|
||||
"""根据fields参数过滤车辆状态字段"""
|
||||
if not fields:
|
||||
return status_data
|
||||
|
||||
# 解析fields参数
|
||||
requested_fields = [f.strip() for f in fields.split(',')]
|
||||
filtered_data = {}
|
||||
|
||||
# 可用的字段组
|
||||
available_fields = {
|
||||
"vehicleInfo", "operationalStatus", "controlStatus", "motionStatus",
|
||||
"safetyStatus", "sensorStatus", "batteryStatus", "communicationStatus"
|
||||
}
|
||||
|
||||
for field in requested_fields:
|
||||
if field in available_fields and field in status_data:
|
||||
filtered_data[field] = status_data[field]
|
||||
|
||||
return filtered_data
|
||||
|
||||
# 无人车状态数据生成函数
|
||||
def generate_unmanned_vehicle_state_data(vehicle_id: str | None = None, is_single: bool = True) -> list[dict[str, Any]]:
|
||||
"""生成无人车状态数据,符合官方API格式"""
|
||||
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分钟->出港循环"""
|
||||
@ -2616,107 +2173,6 @@ def get_aircraft_status():
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/api/v1/vehicles/<vehicle_id>/status', methods=['GET', 'OPTIONS'])
|
||||
def get_vehicle_status_universal(vehicle_id):
|
||||
"""通用无人车状态上报接口 - 符合universal_autonomous_vehicle_api规范"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
|
||||
logging.info(f"🔍 收到车辆状态查询请求: vehicleId={vehicle_id}")
|
||||
|
||||
# JWT认证检查
|
||||
if not check_auth():
|
||||
logging.warning(f"❌ 车辆状态查询认证失败: vehicleId={vehicle_id}")
|
||||
return jsonify({
|
||||
"code": 401,
|
||||
"message": "认证失败",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"data": None
|
||||
}), 401
|
||||
|
||||
# 检查vehicleId格式(字母数字 3~20 位)
|
||||
if not vehicle_id or len(vehicle_id) < 3 or len(vehicle_id) > 20:
|
||||
return jsonify({
|
||||
"code": 400,
|
||||
"message": "Bad Request",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "VALIDATION_ERROR",
|
||||
"details": "vehicleId must be 3-20 alphanumeric characters",
|
||||
"field": "vehicleId"
|
||||
}
|
||||
}), 400
|
||||
|
||||
# 检查车辆是否存在
|
||||
vehicle_exists = False
|
||||
for v in unmanned_vehicle_data:
|
||||
if v["vehicleNo"] == vehicle_id:
|
||||
vehicle_exists = True
|
||||
break
|
||||
|
||||
if not vehicle_exists:
|
||||
return jsonify({
|
||||
"code": 404,
|
||||
"message": "Not Found",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "RESOURCE_NOT_FOUND",
|
||||
"details": f"Vehicle {vehicle_id} not found",
|
||||
"field": "vehicleId"
|
||||
}
|
||||
}), 404
|
||||
|
||||
try:
|
||||
# 更新车辆位置数据
|
||||
current_time = time.time()
|
||||
global last_unmanned_vehicle_update_time
|
||||
elapsed_time = current_time - last_unmanned_vehicle_update_time
|
||||
if elapsed_time >= UPDATE_INTERVAL:
|
||||
for vehicle in unmanned_vehicle_data:
|
||||
update_vehicle_position(vehicle, UPDATE_INTERVAL)
|
||||
vehicle["time"] = int(current_time * 1000)
|
||||
last_unmanned_vehicle_update_time = current_time
|
||||
|
||||
# 生成完整的车辆状态数据
|
||||
status_data = generate_comprehensive_vehicle_status(vehicle_id)
|
||||
if not status_data:
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": "Internal Server Error",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "DATA_ERROR",
|
||||
"details": "Failed to generate vehicle status data",
|
||||
"field": None
|
||||
}
|
||||
}), 500
|
||||
|
||||
# 处理fields查询参数
|
||||
fields = request.args.get('fields')
|
||||
if fields:
|
||||
status_data = filter_vehicle_status_fields(status_data, fields)
|
||||
|
||||
# 返回成功响应
|
||||
logging.info(f"🚗 返回车辆状态数据: vehicleId={vehicle_id}, 电量={status_data.get('batteryStatus', {}).get('mainBattery', {}).get('chargeLevel', '未知')}%")
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"data": status_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in get_vehicle_status_universal: {str(e)}")
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": "Internal Server Error",
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"error": {
|
||||
"type": "SERVER_ERROR",
|
||||
"details": str(e),
|
||||
"field": None
|
||||
}
|
||||
}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='localhost', port=8090, debug=True)
|
||||
@ -16,6 +16,13 @@ import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
import signal
|
||||
from typing import TypedDict
|
||||
|
||||
class TLState(TypedDict):
|
||||
name: str
|
||||
duration: int
|
||||
ns: tuple[int, int, int]
|
||||
ew: tuple[int, int, int]
|
||||
|
||||
class TrafficLightDevice:
|
||||
"""红绿灯设备模拟器"""
|
||||
@ -27,8 +34,8 @@ class TrafficLightDevice:
|
||||
self.running = False
|
||||
self.socket = None
|
||||
|
||||
def create_di_signal(self, ns_red=0, ns_yellow=0, ns_green=0,
|
||||
ew_red=0, ew_yellow=0, ew_green=0):
|
||||
def create_di_signal(self, ns_red: int = 0, ns_yellow: int = 0, ns_green: int = 0,
|
||||
ew_red: int = 0, ew_yellow: int = 0, ew_green: int = 0):
|
||||
"""
|
||||
创建红绿灯DI信号数据(纯JSON格式)
|
||||
|
||||
@ -129,7 +136,7 @@ class TrafficLightDevice:
|
||||
tuple: (状态名称, (ns_red, ns_yellow, ns_green), (ew_red, ew_yellow, ew_green))
|
||||
"""
|
||||
# 红绿灯状态序列,每个状态持续时间(秒)
|
||||
states = [
|
||||
states: list[TLState] = [
|
||||
{"name": "南北红+东西绿", "duration": 30, "ns": (1,0,0), "ew": (0,0,1)},
|
||||
{"name": "南北红+东西黄", "duration": 3, "ns": (1,0,0), "ew": (0,1,0)},
|
||||
{"name": "南北绿+东西红", "duration": 25, "ns": (0,0,1), "ew": (1,0,0)},
|
||||
@ -214,14 +221,14 @@ class TrafficLightDevice:
|
||||
|
||||
print("开始测试各种红绿灯信号状态...")
|
||||
|
||||
test_cases = [
|
||||
{"name": "南北红+东西绿", "ns": (1,0,0), "ew": (0,0,1)},
|
||||
{"name": "南北红+东西黄", "ns": (1,0,0), "ew": (0,1,0)},
|
||||
{"name": "南北绿+东西红", "ns": (0,0,1), "ew": (1,0,0)},
|
||||
{"name": "南北黄+东西红", "ns": (0,1,0), "ew": (1,0,0)},
|
||||
{"name": "全红状态", "ns": (1,0,0), "ew": (1,0,0)},
|
||||
{"name": "无信号状态", "ns": (0,0,0), "ew": (0,0,0)},
|
||||
{"name": "冲突状态(两绿)", "ns": (0,0,1), "ew": (0,0,1)},
|
||||
test_cases: list[TLState] = [
|
||||
{"name": "南北红+东西绿", "duration": 2, "ns": (1,0,0), "ew": (0,0,1)},
|
||||
{"name": "南北红+东西黄", "duration": 2, "ns": (1,0,0), "ew": (0,1,0)},
|
||||
{"name": "南北绿+东西红", "duration": 2, "ns": (0,0,1), "ew": (1,0,0)},
|
||||
{"name": "南北黄+东西红", "duration": 2, "ns": (0,1,0), "ew": (1,0,0)},
|
||||
{"name": "全红状态", "duration": 2, "ns": (1,0,0), "ew": (1,0,0)},
|
||||
{"name": "无信号状态", "duration": 2, "ns": (0,0,0), "ew": (0,0,0)},
|
||||
{"name": "冲突状态(两绿)", "duration": 2, "ns": (0,0,1), "ew": (0,0,1)},
|
||||
]
|
||||
|
||||
try:
|
||||
1005
tools/mock_unmanned_vehicle.py
Normal file
1005
tools/mock_unmanned_vehicle.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -120,6 +120,8 @@
|
||||
<div><strong>红绿灯状态:</strong> <span id="trafficLightStatusCount">0</span></div>
|
||||
<div><strong>路口红绿灯:</strong> <span id="intersectionTrafficLightCount">0</span></div>
|
||||
<div><strong>航空器路由:</strong> <span id="aircraftRouteCount">0</span></div>
|
||||
<div><strong>事件驱动路由:</strong> <span id="eventDrivenRouteCount">0</span></div>
|
||||
<div><strong>定时采集路由:</strong> <span id="scheduledRouteCount">0</span></div>
|
||||
<div><strong>航班进出港:</strong> <span id="flightNotificationCount">0</span></div>
|
||||
<div><strong>路径冲突:</strong> <span id="pathConflictCount">0</span></div>
|
||||
<div><strong>车辆指令:</strong> <span id="vehicleCommandCount">0</span></div>
|
||||
@ -186,6 +188,8 @@
|
||||
traffic_light_status: 0,
|
||||
intersection_traffic_light_status: 0,
|
||||
aircraftRouteUpdate: 0,
|
||||
eventDrivenRoute: 0, // 事件驱动的路由更新
|
||||
scheduledRoute: 0, // 定时采集的路由更新
|
||||
flight_notification: 0,
|
||||
path_conflict_alert: 0,
|
||||
vehicle_command: 0,
|
||||
@ -252,6 +256,8 @@
|
||||
document.getElementById('trafficLightStatusCount').textContent = messageStats.traffic_light_status;
|
||||
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;
|
||||
@ -404,10 +410,32 @@
|
||||
log('collisionLog', `🚦 路口红绿灯状态: ${intersectionId} - 设备${deviceId} - 南北:${nsStatus} 东西:${ewStatus}`, 'info');
|
||||
break;
|
||||
case 'aircraftRouteUpdate':
|
||||
const flightNo = message.payload?.flightNo || '未知航班';
|
||||
const routeType = message.payload?.routeType || '未知路由';
|
||||
const routeStatus = message.payload?.status || '未知状态';
|
||||
log('collisionLog', `航空器路由更新: ${flightNo} - ${routeType} (${routeStatus})`, 'info');
|
||||
const flightNo = message.data?.flightNo || '未知航班';
|
||||
const routeType = message.data?.routeType || '未知路由';
|
||||
const routeStatus = message.data?.routeStatus || '未知状态';
|
||||
const aircraftStatus = message.data?.aircraftStatus || '未知状态';
|
||||
const routeCodes = message.data?.routeCodes || '无编码';
|
||||
const eventSource = message.data?.eventSource || '未知来源';
|
||||
const routeGeometry = message.data?.routeGeometry ? '已提供几何数据' : '无几何数据';
|
||||
|
||||
// 根据事件来源更新分类统计
|
||||
if (eventSource === 'FLIGHT_NOTIFICATION') {
|
||||
messageStats.eventDrivenRoute++;
|
||||
} else if (eventSource === 'SCHEDULED') {
|
||||
messageStats.scheduledRoute++;
|
||||
}
|
||||
|
||||
// 根据事件来源设置不同的日志类型和图标
|
||||
let logType = 'info';
|
||||
let icon = '🛫';
|
||||
if (eventSource === 'FLIGHT_NOTIFICATION') {
|
||||
logType = 'success';
|
||||
icon = '🚀'; // 事件驱动的路由更新用火箭图标
|
||||
} else if (eventSource === 'SCHEDULED') {
|
||||
icon = '🕐'; // 定时采集的路由更新用时钟图标
|
||||
}
|
||||
|
||||
log('collisionLog', `${icon} 航空器路由更新: ${flightNo} | 路由:${routeType} 状态:${routeStatus} | 航空器:${aircraftStatus} | 编码:${routeCodes} | 来源:${eventSource} | ${routeGeometry}`, logType);
|
||||
break;
|
||||
case 'flight_notification':
|
||||
case 'FLIGHT_NOTIFICATION':
|
||||
@ -420,9 +448,9 @@
|
||||
const notificationDescription = message.payload?.eventDescription || message.eventDescription || '无描述';
|
||||
|
||||
// 根据通知级别设置日志类型
|
||||
const logType = notificationLevel === 'IMPORTANT' ? 'warning' : 'info';
|
||||
const notificationLogType = notificationLevel === 'IMPORTANT' ? 'warning' : 'info';
|
||||
|
||||
log('collisionLog', `✈️ 航班进出港通知: ${notificationFlightNo} - ${notificationEventType} (${notificationFlightType}) | 跑道:${notificationRunway} 机位:${notificationSeat} | ${notificationDescription}`, logType);
|
||||
log('collisionLog', `✈️ 航班进出港通知: ${notificationFlightNo} - ${notificationEventType} (${notificationFlightType}) | 跑道:${notificationRunway} 机位:${notificationSeat} | ${notificationDescription}`, notificationLogType);
|
||||
break;
|
||||
case 'path_conflict_alert':
|
||||
const object1 = message.payload?.object1Name || '未知对象1';
|
||||
@ -569,6 +597,8 @@
|
||||
document.getElementById('trafficLightStatusCount').textContent = '0';
|
||||
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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user