From 1fc14d7438ce7934813afc1b4b51e28032a7bb06 Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 27 Jan 2026 15:24:05 +0800 Subject: [PATCH] Initial commit --- .clang-format | 14 + .clangd | 22 + .cursorignore | 3 + .cursorrules | 44 + .gitignore | 47 + CMakeLists.txt | 197 ++++ README.md | 208 +++++ compile_commands.json | 7 + config/airport_bounds.json | 147 +++ config/intersections.json | 34 + config/system_config.json | 165 ++++ config/unmanned_vehicles.json | 22 + docs/airport_bounds_description.md | 83 ++ docs/configuration_guide.md | 366 ++++++++ docs/configuration_guide.pdf | Bin 0 -> 684732 bytes docs/conflict_detection_algorithms.md | 197 ++++ docs/deployment_guide.md | 172 ++++ docs/design.md | 1070 ++++++++++++++++++++++ docs/docker_deployment_design.md | 232 +++++ docs/mock_server.md | 384 ++++++++ docs/official_api.md | 225 +++++ docs/performance_test_report.md | 340 +++++++ docs/requirements.md | 60 ++ docs/warning_strategy.md | 203 +++++ scripts/build_execute_package.sh | 142 +++ scripts/deploy.sh | 256 ++++++ scripts/prepare_deploy.sh | 166 ++++ scripts/uninstall.sh | 56 ++ src/collector/DataCollector.cpp | 471 ++++++++++ src/collector/DataCollector.h | 117 +++ src/collector/DataSource.h | 38 + src/collector/DataSourceConfig.cpp | 72 ++ src/collector/DataSourceConfig.h | 171 ++++ src/config/AirportBounds.cpp | 179 ++++ src/config/AirportBounds.h | 58 ++ src/config/AreaConfig.h | 53 ++ src/config/IntersectionConfig.cpp | 78 ++ src/config/IntersectionConfig.h | 43 + src/config/SystemConfig.cpp | 21 + src/config/SystemConfig.h | 293 ++++++ src/config/WarnConfig.h | 11 + src/core/System.cpp | 856 +++++++++++++++++ src/core/System.h | 126 +++ src/detector/CollisionDetector.cpp | 729 +++++++++++++++ src/detector/CollisionDetector.h | 153 ++++ src/detector/CollisionTypes.h | 47 + src/detector/RectangleCollision.h | 117 +++ src/detector/SafetyZone.cpp | 126 +++ src/detector/SafetyZone.h | 66 ++ src/detector/TrafficLightDetector.cpp | 72 ++ src/detector/TrafficLightDetector.h | 27 + src/main.cpp | 67 ++ src/mock/MockDataService.cpp | 168 ++++ src/mock/MockDataService.h | 43 + src/network/HTTPClient.cpp | 142 +++ src/network/HTTPClient.h | 20 + src/network/HTTPDataSource.cpp | 605 ++++++++++++ src/network/HTTPDataSource.h | 96 ++ src/network/MessageTypes.h | 71 ++ src/network/NetworkInterface.h | 17 + src/network/TrafficLightHttpServer.cpp | 377 ++++++++ src/network/TrafficLightHttpServer.h | 64 ++ src/network/WebSocketServer.cpp | 141 +++ src/network/WebSocketServer.h | 32 + src/spatial/CoordinateConverter.cpp | 52 ++ src/spatial/CoordinateConverter.h | 39 + src/spatial/QuadTree.cpp | 5 + src/spatial/QuadTree.h | 196 ++++ src/types/BasicTypes.cpp | 151 +++ src/types/BasicTypes.h | 113 +++ src/types/TrafficLightTypes.h | 21 + src/types/VehicleCommand.h | 60 ++ src/types/VehicleData.cpp | 6 + src/types/VehicleData.h | 30 + src/utils/Logger.cpp | 7 + src/utils/Logger.h | 99 ++ src/vehicle/ControllableVehicles.cpp | 111 +++ src/vehicle/ControllableVehicles.h | 34 + tests/AirportBoundsTest.cpp | 246 +++++ tests/BasicCollisionTest.cpp | 539 +++++++++++ tests/BasicTypesTest.cpp | 128 +++ tests/CMakeLists.txt | 26 + tests/CollisionDetectorTest.cpp | 322 +++++++ tests/DataCollectorTest.cpp | 906 ++++++++++++++++++ tests/HTTPDataSourceTest.cpp | 286 ++++++ tests/SafetyZoneTest.cpp | 190 ++++ tests/TrafficLightDetectorTest.cpp | 141 +++ tests/data/invalid_airport_bounds.json | 5 + tools/map.html | 303 ++++++ tools/map_websocket.html | 740 +++++++++++++++ tools/mock_server.py | 1162 ++++++++++++++++++++++++ tools/mock_traffic_light_client.py | 109 +++ tools/test_websocket.html | 135 +++ 93 files changed, 16791 insertions(+) create mode 100644 .clang-format create mode 100644 .clangd create mode 100644 .cursorignore create mode 100644 .cursorrules create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 compile_commands.json create mode 100644 config/airport_bounds.json create mode 100644 config/intersections.json create mode 100644 config/system_config.json create mode 100644 config/unmanned_vehicles.json create mode 100644 docs/airport_bounds_description.md create mode 100644 docs/configuration_guide.md create mode 100644 docs/configuration_guide.pdf create mode 100644 docs/conflict_detection_algorithms.md create mode 100644 docs/deployment_guide.md create mode 100644 docs/design.md create mode 100644 docs/docker_deployment_design.md create mode 100644 docs/mock_server.md create mode 100644 docs/official_api.md create mode 100644 docs/performance_test_report.md create mode 100644 docs/requirements.md create mode 100644 docs/warning_strategy.md create mode 100644 scripts/build_execute_package.sh create mode 100644 scripts/deploy.sh create mode 100644 scripts/prepare_deploy.sh create mode 100644 scripts/uninstall.sh create mode 100644 src/collector/DataCollector.cpp create mode 100644 src/collector/DataCollector.h create mode 100644 src/collector/DataSource.h create mode 100644 src/collector/DataSourceConfig.cpp create mode 100644 src/collector/DataSourceConfig.h create mode 100644 src/config/AirportBounds.cpp create mode 100644 src/config/AirportBounds.h create mode 100644 src/config/AreaConfig.h create mode 100644 src/config/IntersectionConfig.cpp create mode 100644 src/config/IntersectionConfig.h create mode 100644 src/config/SystemConfig.cpp create mode 100644 src/config/SystemConfig.h create mode 100644 src/config/WarnConfig.h create mode 100644 src/core/System.cpp create mode 100644 src/core/System.h create mode 100644 src/detector/CollisionDetector.cpp create mode 100644 src/detector/CollisionDetector.h create mode 100644 src/detector/CollisionTypes.h create mode 100644 src/detector/RectangleCollision.h create mode 100644 src/detector/SafetyZone.cpp create mode 100644 src/detector/SafetyZone.h create mode 100644 src/detector/TrafficLightDetector.cpp create mode 100644 src/detector/TrafficLightDetector.h create mode 100644 src/main.cpp create mode 100644 src/mock/MockDataService.cpp create mode 100644 src/mock/MockDataService.h create mode 100644 src/network/HTTPClient.cpp create mode 100644 src/network/HTTPClient.h create mode 100644 src/network/HTTPDataSource.cpp create mode 100644 src/network/HTTPDataSource.h create mode 100644 src/network/MessageTypes.h create mode 100644 src/network/NetworkInterface.h create mode 100644 src/network/TrafficLightHttpServer.cpp create mode 100644 src/network/TrafficLightHttpServer.h create mode 100644 src/network/WebSocketServer.cpp create mode 100644 src/network/WebSocketServer.h create mode 100644 src/spatial/CoordinateConverter.cpp create mode 100644 src/spatial/CoordinateConverter.h create mode 100644 src/spatial/QuadTree.cpp create mode 100644 src/spatial/QuadTree.h create mode 100644 src/types/BasicTypes.cpp create mode 100644 src/types/BasicTypes.h create mode 100644 src/types/TrafficLightTypes.h create mode 100644 src/types/VehicleCommand.h create mode 100644 src/types/VehicleData.cpp create mode 100644 src/types/VehicleData.h create mode 100644 src/utils/Logger.cpp create mode 100644 src/utils/Logger.h create mode 100644 src/vehicle/ControllableVehicles.cpp create mode 100644 src/vehicle/ControllableVehicles.h create mode 100644 tests/AirportBoundsTest.cpp create mode 100644 tests/BasicCollisionTest.cpp create mode 100644 tests/BasicTypesTest.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/CollisionDetectorTest.cpp create mode 100644 tests/DataCollectorTest.cpp create mode 100644 tests/HTTPDataSourceTest.cpp create mode 100644 tests/SafetyZoneTest.cpp create mode 100644 tests/TrafficLightDetectorTest.cpp create mode 100644 tests/data/invalid_airport_bounds.json create mode 100644 tools/map.html create mode 100644 tools/map_websocket.html create mode 100644 tools/mock_server.py create mode 100644 tools/mock_traffic_light_client.py create mode 100644 tools/test_websocket.html diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1ab6e90 --- /dev/null +++ b/.clang-format @@ -0,0 +1,14 @@ +# 精简版 K&R 风格配置 +BasedOnStyle: LLVM +BreakBeforeBraces: Linux +IndentWidth: 4 +AllowShortFunctionsOnASingleLine: Empty + +# 显式禁用所有大括号换行 +BraceWrapping: + AfterClass: false + AfterStruct: false + AfterUnion: false + AfterFunction: false + AfterNamespace: false + AfterControlStatement: Never \ No newline at end of file diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..2463b1a --- /dev/null +++ b/.clangd @@ -0,0 +1,22 @@ +CompileFlags: + Add: + - "-I${workspaceFolder}/src" + - "-I/opt/homebrew/include" + - "-I/opt/homebrew/Cellar/boost/1.86.0_2/include" + - "-I/opt/homebrew/Cellar/nlohmann-json/3.11.3/include" + Remove: + - "-W*" + - "-std=*" + Compiler: clang++ + +Diagnostics: + ClangTidy: + Add: + - modernize* + - performance* + Remove: + - modernize-use-trailing-return-type + UnusedIncludes: Strict + +Index: + Background: Build \ No newline at end of file diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..f90de47 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,3 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +.cursorignore +build/ diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..bb6293d --- /dev/null +++ b/.cursorrules @@ -0,0 +1,44 @@ +请遵循: +- 最小化修改代码,不要修改任何与解决当前问题无关的代码(包括注释),一定不能删除任何其他的代码 +- 使用源代码中的格式化方式和注释风格 +- 日志的字符串格式采用拼接方式,不要使用{}占位符 + +请遵循以下 Markdown 规范: + +1. 标题层级 + - 文档标题使用一级标题 (#) + - 主要章节使用二级标题 (##) + - 子章节使用三级及以下标题 + - 标题和内容之间空一行 + - 标题层级不跳跃使用 + +2. 列表格式 + - 无序列表统一使用 - + - 列表项前后空一行 + - 列表嵌套使用两个空格缩进 + +3. 代码块 + - 使用 ```language 指定语言 + - 代码块前后空一行 + - 代码添加必要的注释 + +4. 内容格式 + - 中英文之间加空格 + - 使用半角标点符号 + - 代码、参数等使用反引号包裹 + - 重要内容使用加粗或斜体 + +5. 文档结构 + - 章节层次分明 + - 内容组织有序 + - 格式统一一致 + +请遵循如下 C++ 规范: +- 使用 C++17 标准 +- 使用 CMake 3.14 及以上版本 +- 使用 nlohmann_json 3.11.3 版本 +- 使用 FetchContent 管理第三方库 +- 使用 Google Test 进行单元测试 +- 使用 Google Mock 进行单元测试 +- 使用 Google Benchmark 进行性能测试 +- 在头文件中定义函数,在源文件中实现函数 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd1febf --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Build directories +build/ +out/ +logs/ +cmake-build-*/ + +# IDE directories +.idea/ +.vscode/ +*.swp +*.swo + +# Compiled files +*.o +*.obj +*.exe +*.dll +*.so +*.dylib + +# CMake generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +Makefile + +# Python cache files +__pycache__/ +*.py[cod] +pyvenv.cfg + +# macOS system files +.DS_Store +.AppleDouble +.LSOverride + +# Log files +*.log + +# Dependency directories +/vendor/ +/third_party/ + +# Test generated files +Testing/ + +assistant_snippet* \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..09c3fdd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,197 @@ +cmake_minimum_required(VERSION 3.14...3.27) + +# 设置编译器 +if(NOT APPLE) + set(CMAKE_C_COMPILER "/usr/local/bin/gcc") + set(CMAKE_CXX_COMPILER "/usr/local/bin/g++") +endif() + +# 设置 CMake 策略(移除非必要的新策略) +cmake_policy(SET CMP0074 NEW) # 保留必要的包根目录策略 + +# 包含 FetchContent 模块 +include(FetchContent) + +# 生成 compile_commands.json +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +project(collision_avoidance) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# 检查编译器版本 +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7.0) + message(FATAL_ERROR "GCC 版本需要 >= 7.0 以支持 C++17") + endif() +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5.0) + message(FATAL_ERROR "Clang 版本需要 >= 5.0 以支持 C++17") + endif() +endif() + +# 设置输出目录 +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +# 设置 Boost +set(Boost_NO_WARN_NEW_VERSIONS 1) +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +if(NOT APPLE) + # 在 CentOS 上使用 Boost 1.69 + set(BOOST_ROOT "/usr/include/boost169") + set(BOOST_LIBRARYDIR "/usr/lib64/boost169") +else() + # 在 macOS 上使用 Homebrew 安装的 Boost + set(BOOST_ROOT "/opt/homebrew/Cellar/boost/1.87.0") + set(BOOST_LIBRARYDIR "/opt/homebrew/Cellar/boost/1.87.0/lib") +endif() + +# 设置 CMake 策略(在 find_package(Boost) 前添加) +if(POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) # 强制使用传统 FindBoost 模块 +endif() + +find_package(Boost 1.69.0 REQUIRED COMPONENTS + system + filesystem + thread + chrono + date_time + atomic + regex # 添加缺失的 regex 组件 +) + +# 添加 libcurl +find_package(CURL REQUIRED) + +# 修改 JSON 库配置为双模式 +if(APPLE) + # macOS 使用 FetchContent + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + ) + FetchContent_MakeAvailable(nlohmann_json) +else() + # CentOS 使用系统安装的包 + find_package(nlohmann_json REQUIRED) + + # 验证系统包路径 + if(NOT TARGET nlohmann_json::nlohmann_json) + message(FATAL_ERROR "系统未安装正确版本的 nlohmann_json") + endif() + + # 添加手动包含路径(适用于旧版CMake) + include_directories(${nlohmann_json_INCLUDE_DIRS}) +endif() + +# 源文件列表 +set(LIB_SOURCES + src/collector/DataCollector.cpp + src/collector/DataSourceConfig.cpp + src/core/System.cpp + src/detector/CollisionDetector.cpp + src/detector/SafetyZone.cpp + src/network/HTTPDataSource.cpp + src/network/WebSocketServer.cpp + src/network/HTTPClient.cpp + src/network/TrafficLightHttpServer.cpp + src/spatial/CoordinateConverter.cpp + src/types/BasicTypes.cpp + src/types/VehicleData.cpp + src/vehicle/ControllableVehicles.cpp + src/config/AirportBounds.cpp + src/config/SystemConfig.cpp + src/config/IntersectionConfig.cpp + src/detector/TrafficLightDetector.cpp +) + +# 创建主库 +add_library(${PROJECT_NAME}_lib ${LIB_SOURCES}) + +target_include_directories(${PROJECT_NAME}_lib + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${Boost_INCLUDE_DIRS} + ${CURL_INCLUDE_DIRS} +) + +target_link_libraries(${PROJECT_NAME}_lib + PUBLIC + Boost::system + Boost::filesystem + Boost::thread + Boost::chrono + Boost::date_time + Boost::atomic + Boost::regex + nlohmann_json::nlohmann_json + CURL::libcurl + Threads::Threads + -pthread +) + +# 创建主可执行文件 +add_executable(${PROJECT_NAME} src/main.cpp) +target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_lib) + +# 添加一个选项来控制是否编译测试 +if(APPLE) + option(BUILD_TESTS "Build test cases" ON) +else() + option(BUILD_TESTS "Build test cases" OFF) +endif() + +# 添加测试 +if(BUILD_TESTS AND EXISTS "${CMAKE_SOURCE_DIR}/tests") + if(APPLE) + # macOS 使用 FetchContent 下载 googletest + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.15.2 + ) + FetchContent_MakeAvailable(googletest) + else() + # CentOS 也使用 FetchContent 下载 googletest + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.15.2 + ) + FetchContent_MakeAvailable(googletest) + endif() + + # 添加 tests 子目录 + add_subdirectory(tests) +endif() + +# 打印配置信息 +message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}") +message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") +message(STATUS "Boost_INCLUDE_DIRS: ${Boost_INCLUDE_DIRS}") +message(STATUS "Boost_LIBRARIES: ${Boost_LIBRARIES}") +message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") +message(STATUS "CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}") + +# 复制配置文件到构建目录 +configure_file(${CMAKE_SOURCE_DIR}/config/system_config.json ${CMAKE_BINARY_DIR}/config/system_config.json COPYONLY) +configure_file(${CMAKE_SOURCE_DIR}/config/intersections.json ${CMAKE_BINARY_DIR}/config/intersections.json COPYONLY) +configure_file(${CMAKE_SOURCE_DIR}/config/airport_bounds.json ${CMAKE_BINARY_DIR}/config/airport_bounds.json COPYONLY) +configure_file(${CMAKE_SOURCE_DIR}/config/unmanned_vehicles.json ${CMAKE_BINARY_DIR}/config/unmanned_vehicles.json COPYONLY) + +# 查找 Threads 包 +find_package(Threads REQUIRED) + +if(NOT APPLE) + target_link_libraries(collision_avoidance + PRIVATE + ${PROJECT_NAME}_lib + ) +endif() \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e819168 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# 机场地面碰撞预警系统(测试平台) + +## 项目简介 + +该系统用于监测和预警机场地面航空器与车辆之间的潜在碰撞风险。通过实时采集和分析位置数据,为机场运营提供安全保障。 +本系统运行在机场测试平台中,用于测试和验证碰撞预警系统的功能可行性。 + +## 功能特性 + +- 实时数据采集:获取航空器和车辆的位置信息 +- 碰撞风险检测:基于安全距离进行碰撞风险评估 +- 运动参数计算:自动计算速度和航向信息 +- 坐标转换:支持地理坐标和平面坐标转换 +- 日志记录:详细的运行日志和警告信息 + +## 系统架构 + +- 数据采集模块:从数据源获取位置信息 +- 碰撞检测模块:分析潜在的碰撞风险 +- 坐标转换模块:处理不同坐标系统 +- 核心数据类型:定义基础的数据结构 +- 性能测试模块:使用 Google Benchmark 进行性能评估 + +## 开发环境 + +- C++17 +- CMake 3.14+ +- Boost 1.86.0 +- nlohmann_json 3.11.3 +- Google Test +- Google Mock +- Google Benchmark + +## 构建说明 + +```bash +# 创建构建目录 +mkdir build && cd build + +# 配置项目(启用测试) +cmake -DBUILD_TESTING=ON .. + +# 编译 +cmake --build . + +# 运行单元测试 +ctest --output-on-failure + +# 运行性能测试 +./bin/benchmark_tests +``` + +## 测试框架 + +- 单元测试:使用 Google Test 框架 +- Mock 测试:使用 Google Mock 进行模拟 +- 性能测试:使用 Google Benchmark +- 测试覆盖: + - 基础数据类型 + - 碰撞检测逻辑 + - 数据采集功能 + - HTTP 数据源 + - 性能基准测试 + +## 目录结构 + +```mermaid +graph TD + A[CollisionAvoidance] --> B[src] + A --> C[tests] + A --> D[tools] + A --> E[docs] + A --> F[build] + A --> G[benchmarks] + + B --> BA[collector] + B --> BB[detector] + B --> BC[network] + B --> BD[spatial] + B --> BE[types] + B --> BF[utils] + B --> BG[core] + + BA --> BAA[DataCollector.h/cpp] + BA --> BAB[DataSource.h] + + BB --> BBA[CollisionDetector.h/cpp] + + BC --> BCA[HTTPDataSource.h/cpp] + BC --> BCB[ConnectionConfig.h] + + BD --> BDA[CoordinateConverter.h/cpp] + + BE --> BEA[BasicTypes.h/cpp] + + BF --> BFA[Logger.h/cpp] + + BG --> BGA[System.h/cpp] + + C --> CA[BasicTypesTest.cpp] + C --> CB[CollisionDetectorTest.cpp] + C --> CC[DataCollectorTest.cpp] + C --> CD[HTTPDataSourceTest.cpp] + + G --> GA[CollisionDetectorBenchmark.cpp] + G --> GB[CoordinateConverterBenchmark.cpp] + + D --> DA[mock_server.py] +``` + +## 依赖管理 + +项目使用 CMake 的 FetchContent 模块管理第三方依赖: + +```cmake +include(FetchContent) + +FetchContent_Declare( + json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz +) +FetchContent_MakeAvailable(json) +``` + +## 配置说明 + +- 数据源配置: + - 主机:localhost + - 端口:8081 + - API路径: + - /api/getCurrentFlightPositions + - /api/getCurrentVehiclePositions + +- 安全参数: + - 水平安全距离:50米 + - 垂直安全距离:50米 + - 最大速度限制: + - 航空器:100米/秒 + - 车辆:30米/秒 + +## 开发团队 + +- 项目负责人:[田建勇] +- 开发人员:[赵豪、陈横、刘青宇] + +## 许可证 + +[License Type] + +## 版本历史 + +- v1.1.0 (2024-03-20) + - 添加 Google Benchmark 性能测试 + - 升级 CMake 最低版本至 3.14 + - 规范化第三方库版本管理 + - 添加 FetchContent 依赖管理 + +- v1.0.0 (2024-11-15) + - 初始版本发布 + - 基本功能实现 + - 单元测试框架搭建 + +## Mock 服务 + +用于模拟机场场面的移动物体(航空器和车辆),提供以下功能: + +- 航空器位置更新 +- 车辆位置更新 +- 红绿灯状态更新 +- 车辆控制指令响应 + +### API 接口 + +- GET /api/getCurrentFlightPositions - 获取航空器位置 +- GET /api/getCurrentVehiclePositions - 获取车辆位置 +- GET /api/getTrafficLightSignals - 获取红绿灯状态 +- POST /api/vehicle/command - 发送车辆控制指令 + +### 运行方式 + +```bash +cd tools +python mock_server.py +``` + +服务默认监听: + +- HTTP 服务: + +### 模拟场景 + +- 航空器:T7 -> T11,到达后返回起点 +- 无人车1 (QN001):T1 -> T2 -> T4,到达后返回起点 +- 无人车2 (QN002):T12 -> T8,到达后返回起点 +- 特勤车 (TQ001):T4 -> T2 -> T3,到达后返回起点 + +### 红绿灯控制 + +- 西路口 (TL001):每10秒切换一次 +- 东路口 (TL002):根据航空器位置自动控制 + +### 车辆控制指令优先级 + +1. ALERT (5) - 告警指令 +2. RED (4) - 红灯指令 +3. WARNING (3) - 预警指令 +4. GREEN (2) - 绿灯状态 +5. RESUME (1) - 恢复指令 diff --git a/compile_commands.json b/compile_commands.json new file mode 100644 index 0000000..204db36 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1,7 @@ +{ + "commands": [ + "cd build", + "cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..", + "ln -s build/compile_commands.json ." + ] +} diff --git a/config/airport_bounds.json b/config/airport_bounds.json new file mode 100644 index 0000000..3d1d153 --- /dev/null +++ b/config/airport_bounds.json @@ -0,0 +1,147 @@ +{ + "airport": { + "rotation_angle": 68.53, + "reference_point": { + "x": 0.0, + "y": 0.0 + }, + "bounds": { + "x": -100, + "y": -200, + "width": 800, + "height": 400 + } + }, + "areas": { + "runway": { + "bounds": { + "x": -101, + "y": -201, + "width": 1, + "height": 1 + }, + "config": { + "collision_radius": { + "aircraft": 100.0, + "special": 50.0, + "unmanned": 25.0 + }, + "height_threshold": 15.0, + "warning_zone_radius": { + "aircraft": 200.0, + "special": 100.0, + "unmanned": 50.0 + }, + "alert_zone_radius": { + "aircraft": 100.0, + "special": 50.0, + "unmanned": 25.0 + } + } + }, + "taxiway": { + "bounds": { + "x": -101, + "y": -201, + "width": 1, + "height": 1 + }, + "config": { + "collision_radius": { + "aircraft": 50.0, + "special": 50.0, + "unmanned": 25.0 + }, + "height_threshold": 10.0, + "warning_zone_radius": { + "aircraft": 100.0, + "special": 100.0, + "unmanned": 50.0 + }, + "alert_zone_radius": { + "aircraft": 50.0, + "special": 50.0, + "unmanned": 25.0 + } + } + }, + "gate": { + "bounds": { + "x": -101, + "y": -201, + "width": 1, + "height": 1 + }, + "config": { + "collision_radius": { + "aircraft": 40.0, + "special": 50.0, + "unmanned": 25.0 + }, + "height_threshold": 5.0, + "warning_zone_radius": { + "aircraft": 80.0, + "special": 100.0, + "unmanned": 50.0 + }, + "alert_zone_radius": { + "aircraft": 40.0, + "special": 50.0, + "unmanned": 25.0 + } + } + }, + "service": { + "bounds": { + "x": -101, + "y": -201, + "width": 1, + "height": 1 + }, + "config": { + "collision_radius": { + "aircraft": 30.0, + "special": 50.0, + "unmanned": 25.0 + }, + "height_threshold": 5.0, + "warning_zone_radius": { + "aircraft": 60.0, + "special": 100.0, + "unmanned": 50.0 + }, + "alert_zone_radius": { + "aircraft": 30.0, + "special": 50.0, + "unmanned": 25.0 + } + } + }, + "test_zone": { + "bounds": { + "x": -100, + "y": -200, + "width": 800, + "height": 400 + }, + "config": { + "collision_radius": { + "aircraft": 30.0, + "special": 15.0, + "unmanned": 15.0 + }, + "height_threshold": 10.0, + "warning_zone_radius": { + "aircraft": 70.0, + "special": 30.0, + "unmanned": 30.0 + }, + "alert_zone_radius": { + "aircraft": 35.0, + "special": 15.0, + "unmanned": 15.0 + } + } + } + } +} \ No newline at end of file diff --git a/config/intersections.json b/config/intersections.json new file mode 100644 index 0000000..c5c0ad8 --- /dev/null +++ b/config/intersections.json @@ -0,0 +1,34 @@ +{ + "intersections": [ + { + "id": "T2路口", + "name": "无人车与特勤车交叉路口", + "trafficLightId": "TL001", + "position": { + "longitude": 120.08502054, + "latitude": 36.35448347, + "altitude": 9.543 + }, + "width": 20.0, + "safetyZone": { + "aircraftRadius": 50.0, + "vehicleRadius": 50.0 + } + }, + { + "id": "T6路口", + "name": "无人车与飞机交叉路口", + "trafficLightId": "TL002", + "position": { + "longitude": 120.08649105, + "latitude": 36.35074527, + "altitude": 9.778 + }, + "width": 30.0, + "safetyZone": { + "aircraftRadius": 50.0, + "vehicleRadius": 50.0 + } + } + ] +} \ No newline at end of file diff --git a/config/system_config.json b/config/system_config.json new file mode 100644 index 0000000..f9ae417 --- /dev/null +++ b/config/system_config.json @@ -0,0 +1,165 @@ +{ + "airport": { + "name": "青岛胶东国际机场", + "iata": "TAO", + "icao": "ZSQD", + "reference_point": { + "latitude": 36.35448347, + "longitude": 120.08502054 + }, + "coordinate_points": [ + { + "point": "T1", + "longitude": 120.0868853, + "latitude": 36.35496367 + }, + { + "point": "T2", + "longitude": 120.08502054, + "latitude": 36.35448347 + }, + { + "point": "T3", + "longitude": 120.08341044, + "latitude": 36.35406879 + }, + { + "point": "T4", + "longitude": 120.08558121, + "latitude": 36.35305878 + }, + { + "point": "T5", + "longitude": 120.08400957, + "latitude": 36.35265197 + }, + { + "point": "T6", + "longitude": 120.08649105, + "latitude": 36.35074527 + }, + { + "point": "T7", + "longitude": 120.08562915, + "latitude": 36.35052372 + }, + { + "point": "T8", + "longitude": 120.08676664, + "latitude": 36.35004529 + }, + { + "point": "T9", + "longitude": 120.08520616, + "latitude": 36.34964473 + }, + { + "point": "T10", + "longitude": 120.08710569, + "latitude": 36.34917893 + }, + { + "point": "T11", + "longitude": 120.0873865, + "latitude": 36.3509885 + }, + { + "point": "T12", + "longitude": 120.08603613, + "latitude": 36.35190217 + }, + { + "point": "T13", + "longitude": 120.08509148, + "latitude": 36.35041247 + } + ] + }, + "data_source": { + "position": { + "host": "localhost", + "port": 8081, + "aircraft_path": "/openApi/getCurrentFlightPositions", + "vehicle_path": "/openApi/getCurrentVehiclePositions", + "refresh_interval_ms": 1000, + "auth": { + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/login", + "auth_required": true + }, + "timeout_ms": 5000, + "read_timeout_ms": 2000 + }, + "unmanned_vehicle": { + "host": "localhost", + "port": 8081, + "location_path": "/api/VehicleLocationInfo", + "status_path": "/api/VehicleStateInfo", + "command_path": "/api/VehicleCommandInfo", + "refresh_interval_ms": 1000, + "auth": { + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/api/login", + "auth_required": false + }, + "timeout_ms": 5000, + "read_timeout_ms": 2000 + }, + "traffic_light": { + "host": "localhost", + "port": 8081, + "signal_path": "/openApi/getTrafficLightSignals", + "refresh_interval_ms": 1000, + "auth": { + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/api/login", + "auth_required": false + }, + "timeout_ms": 5000, + "read_timeout_ms": 2000 + } + }, + "warning": { + "warning_interval_ms": 1000, + "log_interval_ms": 3000 + }, + "websocket": { + "port": 8010, + "max_connections": 100, + "ping_interval_ms": 30000, + "position_update": { + "aircraft_interval_ms": 300, + "vehicle_interval_ms": 500, + "traffic_light_interval_ms": 1000 + } + }, + "collision_detection": { + "update_interval_ms": 200, + "prediction": { + "time_window": 20.0, + "vehicle_size": 20.0, + "aircraft_size": 60.0, + "min_unmanned_speed": 1.0 + } + }, + "logging": { + "level": "debug", + "file": "logs/system.log", + "max_size_mb": 10, + "max_files": 5, + "console_output": true + }, + "debug": { + "enable_mock_data": false, + "save_raw_data": false, + "profile_performance": false + }, + "traffic_light_server": { + "port": 8082, + "max_connections": 100 + }, + "simulated_mobile_light_target_intersection_id": "T2路口" +} \ No newline at end of file diff --git a/config/unmanned_vehicles.json b/config/unmanned_vehicles.json new file mode 100644 index 0000000..4ec60a2 --- /dev/null +++ b/config/unmanned_vehicles.json @@ -0,0 +1,22 @@ +{ + "vehicles": [ + { + "vehicleNo": "QN001", + "type": "UNMANNED", + "ip": "localhost", + "port": 8081 + }, + { + "vehicleNo": "QN002", + "type": "UNMANNED", + "ip": "localhost", + "port": 8081 + }, + { + "vehicleNo": "TQ001", + "type": "SPECIAL", + "ip": "localhost", + "port": 8081 + } + ] +} \ No newline at end of file diff --git a/docs/airport_bounds_description.md b/docs/airport_bounds_description.md new file mode 100644 index 0000000..74e0610 --- /dev/null +++ b/docs/airport_bounds_description.md @@ -0,0 +1,83 @@ +# 机场区域配置说明文档 + +本文档详细说明了 `airport_bounds.json` 配置文件中各参数的含义和用途。 + +## 整体机场边界 (airport.bounds) + +- `x`: 机场区域左上角 X 坐标(米) +- `y`: 机场区域左上角 Y 坐标(米) +- `width`: 机场总宽度(米) +- `height`: 机场总高度(米) + +## 功能区域 (areas) + +### 跑道区域 (runway) + +- 位置参数 (bounds) + - `x`: 跑道起始 X 坐标(米) + - `y`: 跑道起始 Y 坐标(米) + - `width`: 跑道宽度(米) + - `height`: 跑道高度(米) +- 安全配置 (config) + - `vehicle_collision_radius`: 车辆间碰撞检测半径(米) + - `aircraft_ground_radius`: 航空器与车辆碰撞检测半径(米) + - `height_threshold`: 高度阈值(米) + +### 滑行道区域 (taxiway) + +- 位置参数 (bounds) + - `x`: 滑行道起始 X 坐标(米) + - `y`: 滑行道起始 Y 坐标(米) + - `width`: 滑行道宽度(米) + - `height`: 滑行道高度(米) +- 安全配置 (config) + - `vehicle_collision_radius`: 车辆间碰撞检测半径(米) + - `aircraft_ground_radius`: 航空器与车辆碰撞检测半径(米) + - `height_threshold`: 高度阈值(米) + +### 停机位区域 (gate) + +- 位置参数 (bounds) + - `x`: 停机位起始 X 坐标(米) + - `y`: 停机位起始 Y 坐标(米) + - `width`: 停机位区域宽度(米) + - `height`: 停机位区域高度(米) +- 安全配置 (config) + - `vehicle_collision_radius`: 车辆间碰撞检测半径(米) + - `aircraft_ground_radius`: 航空器与车辆碰撞检测半径(米) + - `height_threshold`: 高度阈值(米) + +### 服务区域 (service) + +- 位置参数 (bounds) + - `x`: 服务区起始 X 坐标(米) + - `y`: 服务区起始 Y 坐标(米) + - `width`: 服务区宽度(米) + - `height`: 服务区高度(米) +- 安全配置 (config) + - `vehicle_collision_radius`: 车辆间碰撞检测半径(米) + - `aircraft_ground_radius`: 航空器与车辆碰撞检测半径(米) + - `height_threshold`: 高度阈值(米) + +## 参数说明 + +### 碰撞检测半径 + +- 跑道区域:使用最大的碰撞检测半径,确保最高的安全性 +- 滑行道:使用中等的碰撞检测半径 +- 停机位:使用较小的碰撞检测半径 +- 服务区:使用最小的碰撞检测半径,因为车速较慢且司机更警觉 + +### 高度阈值 + +- 跑道区域:15米,考虑到起降阶段 +- 滑行道:10米,考虑到滑行阶段 +- 停机位和服务区:5米,主要考虑地面操作 + +## 注意事项 + +1. 所有距离单位均为米 +2. 坐标系统使用左下角为原点 +3. 各区域的参数需要根据实际机场情况调整 +4. 碰撞检测半径应考虑车辆和航空器的实际尺寸 +5. 高度阈值应考虑实际运营需求和安全裕度 diff --git a/docs/configuration_guide.md b/docs/configuration_guide.md new file mode 100644 index 0000000..f1b892d --- /dev/null +++ b/docs/configuration_guide.md @@ -0,0 +1,366 @@ +# 机场无人车冲突预警系统配置指南 + +## 目录 +- [机场无人车冲突预警系统配置指南](#机场无人车冲突预警系统配置指南) + - [目录](#目录) + - [一、机场基础信息配置](#一机场基础信息配置) + - [参数说明](#参数说明) + - [配置说明](#配置说明) + - [二、区域边界配置](#二区域边界配置) + - [区域参数规范表](#区域参数规范表) + - [参数说明](#参数说明-1) + - [三、交叉路口配置](#三交叉路口配置) + - [参数说明](#参数说明-2) + - [配置原则](#配置原则) + - [四、数据接口配置](#四数据接口配置) + - [接口参数示例](#接口参数示例) + - [参数说明](#参数说明-3) + - [五、无人车配置](#五无人车配置) + - [参数说明](#参数说明-4) + - [六、预警参数配置](#六预警参数配置) + - [参数说明](#参数说明-5) + - [冲突预警和告警参数](#冲突预警和告警参数) + - [参数说明](#参数说明-6) + - [告警阈值](#告警阈值) + - [七、调试与日志](#七调试与日志) + - [参数说明](#参数说明-7) + - [八、配置验证流程](#八配置验证流程) + - [文档版本](#文档版本) + +--- + +## 一、机场基础信息配置 +文件路径:`config/system_config.json` + +```json +{ + "airport": { + "name": "青岛胶东国际机场", + "iata": "TAO", + "icao": "ZSQD", + "reference_point": { + "latitude": 36.35448347, + "longitude": 120.08502054 + }, + "coordinate_points": [ + {"point": "T1", "longitude": 120.0868853, "latitude": 36.35496367}, + {"point": "T2", "longitude": 120.08502054, "latitude": 36.35448347} + ] + } +} +``` + +### 参数说明 +- **name**:机场名称 +- **iata**:机场三字代码 +- **icao**:机场四字代码 +- **reference_point**:参考点 + - **latitude**:参考点纬度 + - **longitude**:参考点经度 +- **coordinate_points**:坐标点 + - **point**:坐标点名称 + - **longitude**:坐标点经度 + - **latitude**:坐标点纬度 + + +### 配置说明 +- **参考点设置**:选择塔台或机场中心点作为坐标系原点 +- **坐标精度**:经纬度建议保留8位小数(约1cm精度) +- **命名规范**: + - IATA代码使用3位大写字母 + - 坐标点按T1-T13顺序编号 + +--- + +## 二、区域边界配置 +文件路径:`config/airport_bounds.json` + +### 区域参数规范表 +| 区域类型 | 边界坐标x(米) | 边界坐标y(米) | 区域宽度 | 区域高度 | +| -------- | ------------- | ------------- | -------- | -------- | +| 测试区 | -100 | -200 | 800 | 400 | + + +```json + "test_zone": { + "bounds": { + "x": -100, + "y": -200, + "width": 800, + "height": 400 + } +``` + +### 参数说明 +- **bounds**:区域边界 + - **x**:区域左上角x坐标 + - **y**:区域左上角y坐标 + - **width**:区域宽度 + - **height**:区域高度 + +--- + +## 三、交叉路口配置 +文件路径:`config/intersections.json` + +```json +{ + "id": "T2路口", + "name": "无人车与特勤车交叉路口", + "trafficLightId": "TL001", + "position": { + "longitude": 120.08502054, + "latitude": 36.35448347, + "altitude": 9.543 + }, + "width": 20.0, + "safetyZone": { + "aircraftRadius": 50.0, + "vehicleRadius": 50.0 + } +} +``` + +### 参数说明 +- **id**:路口ID +- **name**:路口名称 +- **trafficLightId**:交通灯ID +- **position**:路口位置 +- **width**:路口宽度 +- **safetyZone**:安全区 +- **aircraftRadius**:飞机安全区半径 +- **vehicleRadius**:车辆安全区半径 + +### 配置原则 +1. 路口ID按"T"+数字编号(如T1-T13) +2. 安全区半径建议值: + - 飞机:翼展×1.5 + - 车辆:车长×2.0 + +--- + +## 四、数据接口配置 +文件路径:`config/system_config.json` + +### 接口参数示例 +```json +"data_source": { + "position": { + "host": "localhost", + "port": 8081, + "aircraft_path": "/openApi/getCurrentFlightPositions", + "vehicle_path": "/openApi/getCurrentVehiclePositions", + "refresh_interval_ms": 1000, + "auth": { + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/login", + "auth_required": true + }, + "timeout_ms": 5000, + "read_timeout_ms": 2000 + }, + "unmanned_vehicle": { + "host": "localhost", + "port": 8081, + "location_path": "/api/VehicleLocationInfo", + "status_path": "/api/VehicleStateInfo", + "command_path": "/api/VehicleCommandInfo", + "refresh_interval_ms": 1000, + "auth": { + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/api/login", + "auth_required": false + }, + "timeout_ms": 5000, + "read_timeout_ms": 2000 + }, + "traffic_light": { + "host": "localhost", + "port": 8081, + "signal_path": "/openApi/getTrafficLightSignals", + "refresh_interval_ms": 1000, + "auth": { + "username": "dianxin", + "password": "dianxin@123", + "auth_path": "/api/login", + "auth_required": false + }, + "timeout_ms": 5000, + "read_timeout_ms": 2000 + } + } +``` + +### 参数说明 +- **position**:位置接口 + - **host**:数据源IP地址 + - **port**:数据源端口号 + - **aircraft_path**:航空器数据接口路径 + - **vehicle_path**:车辆数据接口路径 + - **refresh_interval_ms**:数据刷新间隔时间(ms) + - **auth**:认证信息 + - **username**:用户名 + - **password**:密码 + - **auth_path**:认证路径 + - **auth_required**:是否需要认证 + - **timeout_ms**:请求超时时间(ms) + - **read_timeout_ms**:读取超时时间(ms) +- **unmanned_vehicle**:无人车接口 + - **host**:无人车IP地址 + - **port**:无人车端口号 + - **location_path**:无人车位置接口路径 + - **status_path**:无人车状态接口路径 + - **command_path**:无人车命令接口路径 + - **refresh_interval_ms**:数据刷新间隔时间(ms) + - **auth**:认证信息 + - **username**:用户名 + - **password**:密码 + - **auth_path**:认证路径 + - **auth_required**:是否需要认证 + - **timeout_ms**:请求超时时间(ms) + - **read_timeout_ms**:读取超时时间(ms) +- **traffic_light**:交通灯接口 + - **host**:交通灯IP地址 + - **port**:交通灯端口号 + - **signal_path**:交通灯信号接口路径 + - **refresh_interval_ms**:数据刷新间隔时间(ms) + - **auth**:认证信息 + - **username**:用户名 + - **password**:密码 + - **auth_path**:认证路径 + - **auth_required**:是否需要认证 + - **timeout_ms**:请求超时时间(ms) + - **read_timeout_ms**:读取超时时间(ms) + +--- + +## 五、无人车配置 +文件路径:`config/unmanned_vehicles.json` + +```json +"vehicles": [ + { + "vehicleNo": "QN001", + "type": "UNMANNED", + "ip": "localhost", + "port": 8081 + }, + { + "vehicleNo": "QN002", + "type": "UNMANNED", + "ip": "localhost", + "port": 8081 + } + ] +``` + +### 参数说明 +- **vehicleNo**:无人车编号 +- **type**:无人车类型 +- **ip**:无人车IP地址 +- **port**:无人车端口号 + +--- + +## 六、预警参数配置 + +###冲突预测和航空器和车辆尺寸: + +文件路径:`config/system_config.json` + +```json +"collision_detection": { + "update_interval_ms": 200, + "prediction": { + "time_window": 20.0, + "vehicle_size": 20.0, + "aircraft_size": 60.0, + "min_unmanned_speed": 1.0 + } + } +``` +### 参数说明 +- **update_interval_ms**:冲突检测更新间隔时间(ms) +- **prediction**:冲突预测模型参数 + - **time_window**:预测时间窗口(s) + - **vehicle_size**:车辆尺寸(m) + - **aircraft_size**:航空器尺寸(m) + - **min_unmanned_speed**:无人车最小速度(m/s) + +### 冲突预警和告警参数 + +文件路径:`config/airport_bounds.json` + +```json +"warning_zone_radius": { + "aircraft": 70.0, + "special": 30.0, + "unmanned": 30.0 + }, +"alert_zone_radius": { + "aircraft": 35.0, + "special": 15.0, + "unmanned": 15.0 + } +``` + +### 参数说明 +- **warning_zone_radius**:预警区半径 = 预警距离阈值 + 飞机尺寸/2 + 车辆尺寸/2 +- **alert_zone_radius**:告警区半径 = 告警距离阈值 + 飞机尺寸/2 + 车辆尺寸/2 + +### 告警阈值 +| 告警级别 | 飞机距离阈值(m) | 车辆距离阈值(m) | +| -------- | --------------- | --------------- | +| 预警 | 60 | 30 | +| 告警 | 30 | 15 | + +--- + +## 七、调试与日志 +文件路径:`config/logging_config.json` + +```json +"logging": { + "level": "debug", + "file": "logs/system.log", + "max_size_mb": 10, + "max_files": 5, + "console_output": true + } +``` + +### 参数说明 +- **level**:日志级别 +- **file**:日志文件路径 +- **max_size_mb**:日志文件最大大小(MB) +- **max_files**:日志文件最大数量 +- **console_output**:是否输出到控制台 + +--- + +## 八、配置验证流程 + +1. 基础配置检查 +```bash +# 检查配置文件语法 +python3 -m json.tool config/system_config.json +``` + +2. 接口连通性测试 +```bash +curl -X POST http://localhost:8081/openApi/getCurrentFlightPositions +``` + +3. 实时监控 +```bash +tail -f /opt/collision_avoidance/logs/system.log | grep "WARNING" +``` + +## 文档版本 +- 版本号:1.0.0 +- 更新日期:2025-02-06 +- 更新内容:初始版本 + + diff --git a/docs/configuration_guide.pdf b/docs/configuration_guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bac4e09ef19f4a3731de95250af343aafc1dfd38 GIT binary patch literal 684732 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

Ci@E88A~|kI!{yVO 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

8XLfX77}Tjk3yty}I{`*G>NE+ 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 literal 0 HcmV?d00001 diff --git a/docs/conflict_detection_algorithms.md b/docs/conflict_detection_algorithms.md new file mode 100644 index 0000000..9b3d14e --- /dev/null +++ b/docs/conflict_detection_algorithms.md @@ -0,0 +1,197 @@ +# 机场地面冲突检测算法方案 + +## 1. 算法概述 + +机场地面冲突检测系统采用多层次检测策略,结合不同算法的优势,实现快速、准确、可靠的冲突检测。 + +## 2. 常用冲突检测算法 + +### 2.1 基于几何的检测算法 + +#### 2.1.1 CPA (Closest Point of Approach) 算法 + +- 原理: + - 计算两个移动物体之间的最近接近点 + - 预测未来一段时间内的最小距离 + - 判断是否小于安全阈值 + +- 优点: + - 计算简单快速 + - 适合实时系统 + - 易于实现 + +- 缺点: + - 不考虑路网约束 + - 预测精度有限 + +#### 2.1.2 安全区域重叠检测 + +- 原理: + - 为每个物体定义安全区域(通常是椭圆或矩形) + - 检测安全区域是否重叠 + - 考虑速度向量进行预测 + +- 优点: + - 直观易理解 + - 可以考虑物体实际尺寸 + - 适合不同类型交通工具 + +### 2.2 基于时空的检测算法 + +#### 2.2.1 时空立方体算法 + +- 原理: + - 构建 4D 时空轨迹(x, y, z, t) + - 检测时空轨迹的交叉 + - 计算到达同一位置的时间差 + +- 优点: + - 预测性强 + - 可以处理复杂场景 + - 考虑时间维度 + +#### 2.2.2 网格化检测算法 + +- 原理: + - 将场地划分为网格 + - 预测物体在未来时间点占用的网格 + - 检测网格占用冲突 + +- 优点: + - 计算效率高 + - 易于并行处理 + - 适合大规模场景 + +### 2.3 基于概率的检测算法 + +#### 2.3.1 Monte Carlo 模拟 + +- 原理: + - 考虑位置和速度的不确定性 + - 多次随机采样模拟 + - 计算碰撞概率 + +- 优点: + - 考虑不确定性 + - 结果更可靠 + - 适合复杂环境 + +#### 2.3.2 贝叶斯预测模型 + +- 原理: + - 建立运动状态概率模型 + - 实时更新碰撞概率 + - 动态调整预警阈值 + +- 优点: + - 可以学习历史数据 + - 适应性强 + - 预测准确度高 + +## 3. 实现方案 + +### 3.1 多层次检测策略 + +采用三层检测机制: + +1. 第一层:快速检测 + + ```cpp + // 使用简单的 CPA 算法进行快速筛查 + struct CPAResult { + double min_distance; // 最小距离 + double time_to_cpa; // 到达最近点的时间 + Point cpa_point; // 最近接近点 + }; + + CPAResult calculateCPA(const Vehicle& v1, const Vehicle& v2, double prediction_time) { + // 计算两个物体的最近接近点 + // 返回最小距离和时间信息 + } + ``` + +2. 第二层:精确检测 + + ```cpp + // 使用安全区域重叠检测进行精确判断 + struct SafetyZone { + Point center; + double length; + double width; + double orientation; + }; + + bool checkSafetyZoneOverlap(const SafetyZone& zone1, const SafetyZone& zone2) { + // 检查两个安全区域是否重叠 + // 考虑物体的实际尺寸和方向 + } + ``` + +3. 第三层:预测检测 + + ```cpp + // 使用时空网格进行路径预测 + struct TimeSpaceGrid { + int x, y; // 空间坐标 + double time; // 时间戳 + std::vector occupants; // 占用该格子的物体ID + }; + + bool checkPathConflict(const Vehicle& v1, const Vehicle& v2, + const std::vector& grids) { + // 检查未来时间窗口内的路径冲突 + // 考虑路网约束 + } + ``` + +### 3.2 综合判断 + +```cpp +struct ConflictResult { + bool has_conflict; // 是否存在冲突 + double risk_level; // 风险等级 + double time_to_conflict; // 到冲突的时间 + std::string conflict_type; // 冲突类型 +}; + +ConflictResult detectConflict(const Vehicle& v1, const Vehicle& v2) { + // 1. 快速 CPA 检测 + // 2. 如果可能存在冲突,进行安全区域检测 + // 3. 对于高风险情况,进行时空路径预测 + // 4. 综合三层结果,返回最终判断 +} +``` + +## 4. 实现步骤 + +1. 第一阶段:基础功能实现 + - 实现 CPA 算法 + - 实现基本的安全区域检测 + - 设计基础数据结构 + +2. 第二阶段:增强功能 + - 添加时空网格检测 + - 实现路径预测 + - 优化性能 + +3. 第三阶段:高级特性 + - 添加概率模型 + - 实现自适应阈值 + - 系统集成测试 + +## 5. 注意事项 + +1. 性能要求 + - 实时响应:检测延迟不超过 100ms + - CPU 占用率控制在 50% 以下 + - 内存使用不超过 1GB + +2. 可靠性要求 + - 误报率控制在 1% 以下 + - 漏报率控制在 0.1% 以下 + - 系统可用性 99.99% + +3. 扩展性要求 + - 支持新增检测算法 + - 支持参数配置调整 + - 支持不同类型交通工具 diff --git a/docs/deployment_guide.md b/docs/deployment_guide.md new file mode 100644 index 0000000..d59e3b6 --- /dev/null +++ b/docs/deployment_guide.md @@ -0,0 +1,172 @@ +# 部署指南 + +本文档详细说明了如何在 CentOS 系统上部署碰撞避免系统。支持在线和离线两种部署方式。 + +## 目录 + +- [系统要求](#系统要求) +- [在线部署](#在线部署) +- [离线部署](#离线部署) +- [部署后配置](#部署后配置) +- [常见问题](#常见问题) + +## 系统要求 + +- CentOS 7 或更高版本 +- 至少 2GB 可用内存 +- 至少 10GB 可用磁盘空间 +- root 权限 +- Python 3.6 或更高版本 + +## 在线部署 + +如果服务器可以访问互联网,可以直接使用在线部署方式。 + +1. 获取项目代码 + + ```bash + git clone [项目仓库地址] + cd CollisionAvoidance + ``` + +2. 执行部署脚本 + + ```bash + sudo ./scripts/deploy.sh + ``` + +## 离线部署 + +如果服务器无法访问互联网,需要使用离线部署方式。 + +### 准备阶段(在有网络的环境中进行) + +1. 在有网络的 CentOS 环境中,获取项目代码 + + ```bash + git clone [项目仓库地址] + cd CollisionAvoidance + ``` + +2. 执行离线包准备脚本 + + ```bash + ./scripts/prepare_offline_packages.sh + ``` + +脚本会自动完成: + +- 下载所需的系统依赖包 +- 下载 Python 依赖包 +- 创建完整的项目归档文件 + +### 部署步骤 + +1. 将生成的归档文件传输到目标服务器 + + ```bash + scp ../CollisionAvoidance.tar.gz root@target-server:/tmp/ + ``` + +2. 在目标服务器上解压 + + ```bash + cd /tmp + tar xzf CollisionAvoidance.tar.gz + cd CollisionAvoidance + ``` + +3. 删除 32 位依赖包 + + > 注意:由于系统是 64 位架构,我们需要删除所有 32 位(i686)的依赖包,以避免安装冲突。这些包是在准备离线包时自动下载的,但在 64 位系统上不需要。 + + ```bash + # 删除所有 i686 架构的包 + cd packages + rm -f *i686.rpm + cd .. + ``` + +4. 执行部署脚本 + + ```bash + sudo ./scripts/deploy.sh + ``` + +## 部署后配置 + +### 服务管理 + +- 启动服务:`systemctl start collision-avoidance` +- 停止服务:`systemctl stop collision-avoidance` +- 重启服务:`systemctl restart collision-avoidance` +- 查看状态:`systemctl status collision-avoidance` +- 查看日志:`journalctl -u collision-avoidance -f` + +### 配置文件 + +- 配置文件位置:`/etc/collision_avoidance/` +- WebSocket 服务端口:8010 +- Mock Server 端口:8081 + +### 防火墙配置 + +如果使用 firewalld: + +```bash +# 开放 WebSocket 端口 +firewall-cmd --permanent --add-port=8010/tcp +# 开放 Mock Server 端口 +firewall-cmd --permanent --add-port=8081/tcp +firewall-cmd --reload +``` + +## 常见问题 + +### 1. 服务启动失败 + +- 检查服务日志:`journalctl -u collision-avoidance -n 50` +- 确认配置文件权限正确 +- 验证端口 8010 和 8081 是否被占用 + +### 2. Mock Server 启动失�� + +- 检查 Python 和依赖包是否正确安装 +- 确认端口 8081 未被占用 +- 检查 Mock Server 日志输出 + +### 3. 防火墙配置 + +- 如果使用其他防火墙,需要手动开放 8010 和 8081 端口 +- 确认防火墙规则是否生效:`firewall-cmd --list-all` + +### 4. 依赖包安装问题 + +- 如果遇到依赖包冲突,检查是否有 i686 架构的包未被删除 +- 使用 `rpm -qa | grep i686` 命令检查系统中的 32 位包 +- 如果发现 i686 包,使用 `rpm -e` 命令删除它们 + +## 卸载 + +如需卸载系统: + +```bash +sudo ./scripts/uninstall.sh +``` + +卸载脚本会: + +- 停止并禁用服务 +- 删除系统服务配置 +- 删除程序文件 +- 删除配置文件 +- 关闭防火墙端口 + +## 技术支持 + +如遇到问题,请: + +1. 查看服务日志 +2. 检查系统日志 +3. 检查 Mock Server 日志 +4. 联系技术支持团队 diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..26b8ef8 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,1070 @@ +# 机场碰撞预警系统设计文档 + +## 1. 系统概述 + +### 1.1 系统目标 + +实现机场内航空器与车辆的实时位置监控和碰撞预警。 + +### 1.2 主要功能 + +- 实时获取航空器位置数据 +- 实时获取车辆位置数据 +- 碰撞风险检测 +- 预警信息输出 + +## 2. 系统架构 + +### 2.1 整体架构 + +- 数据采集层:负责从外部系统获取位置数据 +- 数据处理层:进行坐标转换和数据规范化 +- 碰撞检测层:执行碰撞风险分析 +- 预警输出层:生成和发送预警信息 + +### 2.2 核心模块 + +1. System:系统总控模块 +2. DataCollector:数据采集模块 +3. CollisionDetector:碰撞检测模块 +4. CoordinateConverter:坐标转换模块 + +## 3. 数据模型 + +### 3.1 基础数据类型 + +```cpp +// 移动物体类型 +enum class MovingObjectType { + AIRCRAFT, // 航空器 + SPECIAL, // 特勤车 + UNMANNED // 无人车(可控车辆) +}; + +// 二维向量 +struct Vector2D { + double x; // 东西方向(米) + double y; // 南北方向(米) + + double magnitude() const; // 计算向量大小 + double direction() const; // 计算向量方向 +}; + +// 地理坐标 +struct GeoPosition { + double latitude; // 纬度 + double longitude; // 经度 +}; + +// 位置记录 +struct PositionRecord { + GeoPosition geo; // 地理位置 + uint64_t timestamp; // 时间戳 +}; + +// 移动物体基类 +class MovingObject { +public: + std::string id; // 唯一标识 + Vector2D position; // 平面坐标 + GeoPosition geo; // 地理坐标 + double heading; // 航向角 + double speed; // 速度 + int64_t timestamp; // 时间戳 + MovingObjectType type; // 物体类型 + std::deque positionHistory; // 位置历史记录 + + virtual bool isValidPosition(const GeoPosition& newPos) const = 0; + virtual bool isValidSpeed(double speed) const = 0; + void updateMotion(const GeoPosition& newPos, uint64_t newTime); +}; +``` + +### 3.2 业务数据类型 + +```cpp +// 航空器 +class Aircraft : public MovingObject { +public: + std::string flightNo; // 航班号 + std::string trackNumber; // 航迹号 + double altitude; // 高度(米) + + static constexpr double MAX_SPEED = 100.0; // 最大速度(米/秒) + static constexpr double MAX_POSITION_JUMP = 50.0; // 最大位置跳变(米) + + bool isValidPosition(const GeoPosition& newPos) const override; + bool isValidSpeed(double speed) const override; +}; + +// 车辆 +class Vehicle : public MovingObject { +public: + std::string vehicleNo; // 车牌号 + bool isControllable; // 是否可控 + + static constexpr double MAX_SPEED = 20.0; // 最大速度(米/秒) + static constexpr double MAX_POSITION_JUMP = 10.0; // 最大位置跳变(米) + + bool isValidPosition(const GeoPosition& newPos) const override; + bool isValidSpeed(double speed) const override; +}; +``` + +### 3.3 配置数据类型 + +```cpp +// 可控车辆配置 +struct ControllableVehicleConfig { + std::string vehicleNo; // 车牌号 + std::string ip; // 车辆IP地址 + int port; // 车辆端口号 +}; +``` + +## 4. 接口设计 + +### 4.1 数据源接口 + +```cpp +class DataSource { + virtual std::vector getAircraftData() = 0; + virtual std::vector getVehicleData() = 0; +}; +``` + +### 4.2 碰撞检测接口 + +```cpp +class CollisionDetector { + bool detectAircraft(const Aircraft& a1, const Aircraft& a2); + bool detectAircraftVehicle(const Aircraft& aircraft, const Vehicle& vehicle); +}; +``` + +## 5. 碰撞检测算法 + +### 5.1 碰撞类型 + +系统支持以下碰撞类型: + +1. HEAD_ON:相向碰撞(航向差大于 150 度) +2. CROSSING:交叉碰撞(航向差在 15-165 度之间) +3. PARALLEL:平行碰撞(航向差小于 30 度) +4. STATIC:静止碰撞(双方速度接近 0) + +### 5.2 风险等级 + +```cpp +enum class RiskLevel { + NONE = 0, // 无风险 + WARNING = 1, // 预警:进入预警区域 + CRITICAL = 2 // 告警:进入危险区域 +}; + +enum class WarningZoneType { + NONE = 0, // 无风险区域 + WARNING = 1, // 预警区域 + DANGER = 2 // 危险区域 +}; +``` + +### 5.3 碰撞检测流程 + +1. 距离检测: + + ```cpp + // 计算当前距离 + double dx = obj2.position.x - obj1.position.x; + double dy = obj2.position.y - obj1.position.y; + double current_distance = std::sqrt(dx*dx + dy*dy); + + // 如果当前距离大于警告区域,直接返回不会碰撞 + if (current_distance > (warning_radius1 + warning_radius2)) { + return false; + } + ``` + +2. 碰撞类型判断: + + ```cpp + double angle_diff = getAngleDifference(heading1, heading2); + if (angle_diff > 150.0) { + // 相向碰撞检测 + type = CollisionType::HEAD_ON; + } else if (angle_diff > 15.0 && angle_diff < 165.0) { + // 交叉碰撞检测 + type = CollisionType::CROSSING; + } else { + // 平行碰撞检测 + type = CollisionType::PARALLEL; + } + ``` + +3. 相对运动分析: + + ```cpp + // 计算速度分量 + double vx1 = speed1 * std::cos((90.0 - heading1) * M_PI / 180.0); + double vy1 = speed1 * std::sin((90.0 - heading1) * M_PI / 180.0); + double vx2 = speed2 * std::cos((90.0 - heading2) * M_PI / 180.0); + double vy2 = speed2 * std::sin((90.0 - heading2) * M_PI / 180.0); + + // 计算相对运动 + double rel_vx = vx2 - vx1; + double rel_vy = vy2 - vy1; + double rel_speed = std::sqrt(rel_vx*rel_vx + rel_vy*rel_vy); + ``` + +4. 碰撞预测: + + ```cpp + // 在时间窗口内采样检查 + for (int i = 1; i <= STEPS; ++i) { + double t = i * dt; + + // 计算t时刻的位置 + Vector2D future1 = predictPosition(pos1, speed1, heading1, t); + Vector2D future2 = predictPosition(pos2, speed2, heading2, t); + + // 计算t时刻的距离 + double distance = calculateDistance(future1, future2); + + // 检查是否会碰撞 + if (distance <= safe_distance) { + return true; + } + } + ``` + +### 5.4 碰撞记录管理 + +```cpp +struct CollisionRecord { + Vector2D collisionPoint; // 冲突点位置 + int64_t detectedTime; // 检测到冲突的时间 + RiskLevel maxLevel; // 历史最高风险等级 + bool resolved; // 是否已解除 +}; +``` + +碰撞解除条件: + +1. 对于已通过交叉点的情况: + - 物体距离交叉点的距离大于安全距离 +2. 对于未通过交叉点的情况: + - 两个物体都在安全距离之外 + - 预测不会发生碰撞 + +### 5.5 区域特定阈值 + +不同区域采用不同的安全距离阈值: + +| 区域 | 航空器地面阈值 | 车辆阈值 | +|----------|----------------|-----------| +| 跑道 | 100米 | 50米 | +| 滑行道 | 50米 | 30米 | +| 停机位 | 40米 | 20米 | +| 服务区 | 30米 | 15米 | + +### 5.6 空间优化 + +使用四叉树进行空间索引,优化查询性能: + +1.四叉树构建: + +```cpp +QuadTree vehicleTree_(bounds, 8); // 容量为8的四叉树 +``` + +2.邻近查询: + +```cpp +auto nearbyVehicles = vehicleTree_.queryNearby( + position, // 中心点 + threshold // 查询半径 +); +``` + +### 5.7 性能考虑 + +1. 空间复杂度: + - 四叉树:O(n),其中 n 为车辆数量 + - 航空器列表:O(m),其中 m 为航空器数量 + +2. 时间复杂度: + - 四叉树查询:O(log n) + - 总体碰撞检测:O(m * log n) + +### 5.8 安全保障 + +1. 距离冗余: + - 使用阈值的一半作为直接报警条件 + - 为不同区域设置合适的安全距离 + +2. 运动预测: + - 考虑物体的相对运动 + - 提前发现潜在碰撞风险 + +3. 降级处理: + - 当无法计算相对运动时,仅使用距离判断 + - 保证基本的安全检测功能 + +### 5.9 碰撞检测主流程 + +```cpp +// 加载可控车辆配置 +std::vector controllableVehicles = loadControllableVehicleConfig(); + +for (const auto& aircraft : aircrafts) { + for (const auto& vehicle : vehicles) { + if (detectCollision(aircraft, vehicle)) { + // 检查是否为可控车辆 + auto iter = std::find_if(controllableVehicles.begin(), controllableVehicles.end(), + [&](const ControllableVehicleConfig& config) { + return config.vehicleNo == vehicle.vehicleNo; + }); + + if (iter != controllableVehicles.end()) { + // 生成控制指令 + auto command = generateCommand(aircraft, vehicle); + // 发送控制指令 + sendCommand(command, iter->ip, iter->port); + } else { + // 发送普通预警 + sendWarning(aircraft, vehicle); + } + } + } +} +``` + +### 5.10 可控车辆配置加载 + +从配置文件加载可控车辆信息: + +```cpp +std::vector loadControllableVehicleConfig() { + std::vector configs; + + // 读取配置文件 + std::ifstream file("controllable_vehicles.json"); + nlohmann::json jsonConfig; + file >> jsonConfig; + + // 解析配置项 + for (const auto& item : jsonConfig["vehicles"]) { + ControllableVehicleConfig config; + config.vehicleNo = item["vehicleNo"].get(); + config.ip = item["ip"].get(); + config.port = item["port"].get(); + configs.push_back(config); + } + + return configs; +} +``` + +配置文件 `controllable_vehicles.json` 的格式如下: + +```json +{ + "vehicles": [ + { + "vehicleNo": "VEH001", + "ip": "192.168.1.101", + "port": 8080 + }, + { + "vehicleNo": "VEH002", + "ip": "192.168.1.102", + "port": 8080 + }, + { + "vehicleNo": "VEH003", + "ip": "192.168.1.103", + "port": 8080 + } + ] +} +``` + +## 6. 坐标转换 + +### 6.1 转换方法 + +- 使用等角投影方法 +- 基于机场参考点(青岛胶东国际机场:北纬36°21'43.2",东经120°05'16.8") +- 考虑地球曲率影响 +- WGS84椭球体参数 + +## 7. 数据更新策略 + +### 7.1 数据采集 + +- 定时轮询(默认1秒) +- 独立的数据采集线程 +- 分别缓存航空器和车辆数据 + +### 7.2 碰撞检测 + +- 实时检测(默认100ms) +- 独立的检测线程 +- 分类处理不同类型的碰撞风险 + +## 8. 开发规范 + +### 8.1 代码规范 + +- Google C++ Style +- 智能指针管理 +- 异常安全 +- RAII原则 + +### 8.2 性能规范 + +- 避免虚函数滥用 +- 减少动态内存分配 +- 合理使用模板 +- 注意数据对齐 + +### 8.3 网络编程规范 + +#### 8.3.1 数据读取策略 + +- 使用分块读取代替一次性读取 +- 合理设置分块大小(如1KB) +- 正确处理部分读取的情况 +- 验证数据完整性 + +#### 8.3.2 错误处理 + +- 实现请求重试机制 +- 设置合理的超时时间 +- 正确处理连接断开 +- 完善的错误日志记录 + +#### 8.3.3 连接管理 + +- 及时关闭无用连接 +- 正确处理连接状态 +- 实现连接池(如需要) +- 处理并发连接 + +#### 8.3.4 性能考虑 + +- 使用异步操作处理并发 +- 实现数据缓存机制 +- 免频繁建立连接 +- 合理控制缓冲区大小 + +#### 8.3.5 最佳实践 + +```cpp +// 分块读取示例 +const size_t chunk_size = 1024; // 1KB per chunk +while (total_read < content_length) { + size_t to_read = std::min(chunk_size, content_length - total_read); + // 读取数据块 + // 处理错误 + // 更新计数 +} + +// 重试机制示例 +const int max_retries = 3; +for (int retry = 0; retry < max_retries; ++retry) { + try { + // 尝试操作 + break; // 成功则退出 + } catch (...) { + if (retry == max_retries - 1) throw; // 最后一次重试失败 + // 等待后重试 + } +} +``` + +### 8.4 HTTP 数据读取规范 + +#### 8.4.1 问题描述 + +在使用 Boost.Asio 读取 HTTP 响应时,可能会遇到 "End of file" 错误。这个错误通常发生在: + +- 读取响应体时 +- 服务器已关闭连接 +- 数据未完全读取 + +#### 8.4.2 错误原因分析 + +1. HTTP 响应头读取时可能已包含部分响应体数据 +2. 忽略缓冲区中的这部分数据会导致: + - 数据丢失 + - 读取位置错误 + - 连接提前关闭 + +#### 8.4.3 解决方案 + +1. 正确处理响应数据: + +```cpp +// 1. 读取响应头 +size_t header_length = asio::read_until(*socket_, response_buf, "\r\n\r\n"); + +// 2. 处理缓冲区中的响应体数据 +size_t body_part = response_buf.size() - header_length; +if (body_part > 0) { + // 提取已缓冲的响应体数据 + body.append( + asio::buffers_begin(response_buf.data()) + header_length, + asio::buffers_end(response_buf.data()) + ); + total_read += body_part; +} + +// 3. 继续读取剩余数据 +while (total_read < content_length) { + // 读取剩余的响应体 +} +``` + +#### 8.4.4 最佳实践 + +1. 数据读取原则: + - 使用简单直接的读取逻辑 + - 准确跟踪已读取的数据量 + - 正确处理缓冲区中的所有数据 + - 避免复杂的重试机制 + +2. 错误处理: + - 及时检查错误码 + - 提供详细的错误日志 + - 在错误发生时及时关闭连接 + - 保持连接状态的一致性 + +3. 数据完整性: + - 验证 Content-Length + - 确保读取完整的响应体 + - 避免数据丢失或重复 + - 正确处理分块数据 + +4. 日志记录: + - 记录关键操作步骤 + - 包含详细的错误信息 + - 记录数据读取进度 + - 便于问题诊断和调试 + +## 9. 测试策略 + +### 9.1 单元测试 + +- 模块功能测试 +- 边界条件测试 +- 异常处理测试 +- 内存泄漏测试 +- 坐标转换精度测试 + +### 9.2 性能测试 + +- 延迟测试 +- 并发测试 +- 压力测试 +- 内存使用测试 +- 坐标转换性能测试 + +## 9. 测试场景 + +### 9.1 碰撞风险场景模拟 + +#### 9.1.1 场景描述 + +模拟一个典型的地面碰撞风险场景: + +- 航空器在跑道上滑行 +- 地面车辆垂直接近跑道 +- 两者轨迹存在交叉点 +- 如不采取措施将发生碰撞 + +#### 9.1.2 场景参数 + +1. 航空器参数: + - 初始位置:跑道西端 (AIRPORT_LON - 0.002, AIRPORT_LAT) + - 运动方向:向东滑行 + - 运动速度:0.0002经度/秒 + - 高度:5米(地面滑行高度) + +2. 车辆参数: + - 初始位置:跑道南侧 (AIRPORT_LON, AIRPORT_LAT - 0.002) + - 运动方向:向北行驶 + - 运动速度:0.0002纬度/秒 + - 与航空器轨迹垂直相交 + +3. 时间参数: + - 更新频率:每秒一次 + - 场景周期:10秒 + - 碰撞风险时间:约5秒(场景中点) + +#### 9.1.3 数据特点 + +1. 位置数据: + - 只提供实时位置信息 + - 不包含速度信息 + - 不包含方向信息 + - 位置精度:小数点后6位 + +2. 数据格式: + +```json +// 航空器数据 +{ + "flightNo": "CES2501", + "longitude": 120.088003, + "latitude": 36.361999, + "time": 1700123456, +} + +// 车辆数据 +{ + "vehicleNo": "VEH001", + "longitude": 120.088003, + "latitude": 36.361999, + "time": 1700123456 +} +``` + +#### 9.1.4 预期结果 + +1. 碰撞检测: + - 在交叉点附近应触发预警 + - 预警时间:碰撞前约5秒 + - 持续时间:直到危险解除 + +2. 风险等级: + - 距离大于100米:低风险 + - 距离50-100米:中等风险 + - 距离小于50米:高风险 + +3. 系统响应: + - 实时计算相对距离 + - 及时发出预警信息 + - 记录完整的碰撞风险过程 + +## 10. 风险控制 + +### 10.1 技术风险 + +- 实时性保证 +- 内存管理 +- 多线程同步 +- 系统稳定性 +- 坐标转换精度 + +### 10.2 解决方案 + +- 性能优化 +- 资源监控 +- 故障恢复 +- 降级处理 +- 坐标转换验证 + +## 11. 后续优化 + +1. 引入GPU加速计算 +2. 优化空间索引算法 +3. 改进故障检测机制 +4. 增加自适应负载均衡 +5. 完善监控告警系统 +6. 优化坐标转换算法 +7. 支持多种投影方式 + +使用纯C++实现可以获得: + +1. 最佳的实时性能 +2. 直接的硬件控制 +3. 精确的内存管理 +4. 最小的系统开销 +5. 可预测的行为表现 + +## 10. 单元测试 + +### 10.1 测试框架 + +- 使用 Google Test 框架 +- 支持 EXPECT 和 ASSERT 断言 +- 支持 Mock 对象 +- 支持测试夹具(Test Fixtures) + +### 10.2 测试用例组织 + +#### 10.2.1 基础类型测试 (BasicTypesTest) + +1. Vector2D 测试 + - 测试向量大小计算 + - 测试方向角计算 + +2. MovingObject 测试 + - 测试速度计算 + - 测试航向角计算 + - 验证位置历史记录 + +3. 数据有效性测试 + - 测试航空器数据 + - 测试车辆数据 + - 验证速度和位置限制 + +#### 10.2.2 碰撞检测测试 (CollisionDetectorTest) + +1. 安全距离测试 + - 测试水平安全距离 + - 测试垂直安全距离 + +2. 边界条件测试 + - 测试临界距离 + - 测试高度边界 + +3. 特殊情况测试 + - 测试零距离情况 + - 测试对角线距离 + +#### 10.2.3 数据采集测试 (DataCollectorTest) + +1. 速度计算测试 + + ```cpp + // 测试数据生成 + double lon = 120.08 + (LON_CHANGE_PER_SEC * i); // 每秒经度变化 + uint64_t timestamp = baseTime + (i * 1); // 每秒一个点 + + // 验证速度计算 + EXPECT_NEAR(aircraft.speed, 55.0, 5.0); // 允许5m/s误差 + ``` + +2. Mock 数据源 + + ```cpp + class MockDataSource : public DataSource { + MOCK_METHOD(bool, connect, (), (override)); + MOCK_METHOD(bool, fetchAircraftData, (std::vector&), (override)); + MOCK_METHOD(bool, fetchVehicleData, (std::vector&), (override)); + }; + ``` + +### 10.3 测试数据生成 + +#### 10.3.1 位置数据 + +1. 航空器数据 + - 基于青岛胶东机场坐标 + - 模拟地面滑行场景 + - 生成连续位置点 + +2. 车辆数据 + - 模拟地面车辆运动 + - 生成可能的碰撞场景 + +#### 10.3.2 时间序列 + +1. 时间戳生成 + - 使用 Unix 时间戳 + - 每秒更新一次 + - 保证时间连性 + +2. 数据更新频率 + - 模拟实际系统的更新频率 + - 确保数据连续性 + - 避免时间戳重复 + +### 10.4 测试覆盖率 + +1. 基础功能测试 + - 坐标转换 + - 速度计算 + - 航向计算 + +2. 错误处理测试 + - 无效数据处理 + - 连接错误处理 + - 数据解析错误 + +3. 边界条件测试 + - 最大/最小速度 + - 位置跳变限制 + - 时间戳异常 + +### 10.5 测试最佳实践 + +1. 测试数据准备 + - 使用实际的地理坐标 + - 模拟真实的运动场景 + - 考虑数据精度 + +2. Mock 对象使用 + - 模拟外部依赖 + - 控制测试环境 + - 验证交互逻辑 + +3. 日志记录 + - 记录测试过程 + - 输出中间结果 + - 便于问题诊断 + +## 11. 可控车辆通信 + +### 11.1 通信方式 + +使用 HTTP 协议与可控车辆通信: + +- 碰撞检测系统作为 HTTP 客户端,向可控车辆发送 POST 请求 +- 请求地址格式为: `http://:/command` +- 请求 Body 中包含控制指令的 JSON 表示 +- 可控车辆作为 HTTP 服务器,接收并处理控制指令 + +### 11.2 指令格式 + +使用 JSON 格式表示控制指令: + +```json +POST /command HTTP/1.1 +Host: : +Content-Type: application/json + +{ + "command": "stop", + "timestamp": 1700123456 +} +``` + +或者: + +```json +POST /command HTTP/1.1 +Host: : +Content-Type: application/json + +{ + "command": "change_route", + "route": [ + {"longitude": 120.08, "latitude": 36.36}, + {"longitude": 120.09, "latitude": 36.37}, + {"longitude": 120.10, "latitude": 36.38} + ], + "timestamp": 1700123456 +} +``` + +### 11.3 可控车辆响应 + +可控车辆收到指令后,需要及时响应: + +1. 立即执行停车、减速等指令 +2. 如果是改变路线,则规划新的路径并切换 +3. 将执行结果以 JSON 格式返回: + +```json +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "status": "ok", + "message": "Command executed successfully", + "timestamp": 1700123456 +} +``` + +或者: + +```json +HTTP/1.1 500 Internal Server Error +Content-Type: application/json + +{ + "status": "error", + "message": "Failed to execute command", + "error": "No route found", + "timestamp": 1700123456 +} +``` + +### 11.4 安全考虑 + +由于 HTTP 是明文传输协议,存在安全隐患。需要采取以下措施: + +1. 使用 HTTPS 代替 HTTP,实现加密通信 +2. 对可控车辆的访问进行身份验证,防止非法控制 +3. 对控制指令进行数字签名,防止指令被篡改 +4. 对敏感数据(如路径点坐标)进行加密,防止泄露 + +### 11.5 可用性考虑 + +为保证可控车辆的控制链路高可用,需要: + +1. 监控可控车辆的 HTTP 服务可用性,发现异常及时告警 +2. 对控制指令的发送进行重试和超时处理,避免单次请求失败导致控制中断 +3. 考虑引入备用的控制方式(如 MQTT),作为 HTTP 不可用时的降级方案 + +### 11.6 指令发送 + +根据可控车辆的IP地址和端口号,发送控制指令: + +```cpp +void sendCommand(const ControlCommand& command, const std::string& ip, int port) { + // 创建HTTP客户端 + HttpClient client(ip, port); + + // 构造请求 + HttpRequest request; + request.setMethod(HttpRequest::POST); + request.setPath("/command"); + request.setHeader("Content-Type", "application/json"); + request.setBody(command.toJson()); + + // 发送请求 + HttpResponse response = client.send(request); + + // 处理响应 + if (response.getStatus() == HttpResponse::OK) { + // 指令发送成功 + logger.info("Command sent successfully"); + } else { + // 指令发送失败 + logger.error("Failed to send command, status code: {}", response.getStatus()); + } +} +``` + +使用 HTTP 客户端库,向可控车辆发送 POST 请求。请求路径为 `/command`,请求体为控制指令的 JSON 表示。根据响应状态码判断指令是否发送成功。 + +## 遗留问题 + +### 1. Python 包依赖问题 + +- 当前部署脚本在 Python 3.6 环境下存在依赖问题 +- 需要添加 importlib-metadata 和 zipp 包 +- 需要调整包版本以兼容 Python 3.6 +- 考虑使用系统自带的 Python 包替代 pip 安装 + +### 2. 部署脚本改进 + +- prepare_deploy.sh 中下载 Python 包时需要包含所有依赖 +- deploy.sh 中需要按正确顺序安装 Python 包 +- 考虑使用 requirements.txt 管理 Python 依赖 +- 需要优化部署脚本的错误处理和日志输出 + +### 3. 生产环境适配 + +- 需要适配离线环境的部署需求 +- 需要完善 SELinux 相关的配置 +- 需要优化日志文件的管理 +- 需要完善服务启动失败的故障排查机制 + +### 4. 后续优化建议 + +- 简化部署步骤,减少人工干预 +- 添加部署回滚机制 +- 添加部署验证步骤 +- 完善部署文档 +- 简化部署步骤,减少人工干预 +- 添加部署回滚机制 +- 添加部署验证步骤 +- 完善部署文档 + +## 机场边界设计 + +### 问题描述 + +实际机场通常是倾斜的(如东南-西北走向),需要一个合适的方案来判断车辆和飞机是否在机场各个区域内。 + +### 设计方案 + +#### 坐标系统设计 + +1. 世界坐标系 + - 使用标准的笛卡尔坐标系 + - X 轴指向东,Y 轴指向北 + +2. 机场局部坐标系 + - 原点:可配置的机场参考点 + - 方向:通过旋转角度将世界坐标系对齐到机场主轴 + - 旋转规则: + - 以正北方向为基准 + - 正值表示逆时针旋转 + - 负值表示顺时针旋转 + - 例:东南-西北走向的机场(以青岛国际机场为例),从东南指向西北为 -21.47 度 + +#### 核心组件 + +1. `AirportBounds` 类 + - 负责管理机场边界和区域定义 + - 提供坐标转换功能 + - 判断位置所属区域 + +2. 配置文件 (`airport_bounds.json`) + + ```json + { + "airport": { + "rotation_angle": -45.0, // 机场旋转角度(度) + "reference_point": { // 机场参考点 + "x": 0.0, + "y": 0.0 + }, + "bounds": { // 机场总边界 + "x": -2000, + "y": -1000, + "width": 4000, + "height": 2000 + } + }, + "areas": { // 各功能区定义 + // ... 具体区域配置 + } + } + ``` + +#### 坐标转换流程 + +1. 位置判断流程 + - 接收世界坐标系中的点坐标 + - 将点坐标转换到机场局部坐标系 + - 在局部坐标系中进行边界检查 + +2. 坐标转换算法 + + ```cpp + Vector2D toAirportCoordinate(const Vector2D& point) { + // 1. 平移到参考点为原点 + double dx = point.x - referencePoint_.x; + double dy = point.y - referencePoint_.y; + + // 2. 应用旋转变换 + double cos_angle = std::cos(rotationAngle_); + double sin_angle = std::sin(rotationAngle_); + + return Vector2D{ + dx * cos_angle + dy * sin_angle, + -dx * sin_angle + dy * cos_angle + }; + } + ``` + +### 优势 + +1. 实现简单,易于理解和维护 +2. 计算开销小 +3. 不需要修改现有的碰撞检测逻辑 +4. 可以方便地调整机场方向 +5. 支持不同区域的差异化配置 + +### 使用说明 + +1. 确定机场实际旋转角度 +2. 选择合适的参考点(通常是机场中心或跑道中心) +3. 在机场局部坐标系中定义各个区域的边界 +4. 配置各区域的安全参数(如碰撞半径、警告区域等) + +### 注意事项 + +1. 角度配置采用度数(而非弧度) +2. 顺时针旋转使用负角度 +3. 确保参考点选择合理,便于区域划分 +4. 区域边界在局部坐标系中定义 + +如果需要计算角度(比如判断车辆朝向),则使用向量点积方法 diff --git a/docs/docker_deployment_design.md b/docs/docker_deployment_design.md new file mode 100644 index 0000000..28ee624 --- /dev/null +++ b/docs/docker_deployment_design.md @@ -0,0 +1,232 @@ +# Docker 部署设计文档 + +本文档详细说明碰撞避免系统的 Docker 部署方案,包括测试环境和生产环境的部署流程。 + +## 环境说明 + +### 测试环境 + +- CentOS 7 +- 可以访问互联网 +- 可以安装软件包 +- 内存 >= 4GB +- 磁盘空间 >= 20GB + +### 生产环境 + +- CentOS 7 +- 不能访问互联网 +- 不能在线安装软件包 +- 内存 >= 8GB +- 磁盘空间 >= 50GB + +## 测试环境工作 + +### 1. 环境准备 + +```bash +# 安装 Docker +curl -fsSL https://get.docker.com | sh +systemctl enable docker +systemctl start docker + +# 安装 Docker Compose +curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose +``` + +### 2. 镜像构建与测试 + +1. 构建基础镜像 +2. 运行自动化测试 +3. 验证功能完整性 +4. 检查资源使用情况 +5. 验证日志收集 +6. 测试健康检查机制 + +### 3. 准备生产部署包 + +1. 导出 Docker 镜像 + +```bash +# 保存基础镜像 +docker save centos:7 > centos7-base.tar + +# 保存应用镜像 +docker save collision-avoidance:latest > collision-avoidance.tar +``` + +2. 准备离线安装包 + +```bash +# 创建部署包目录 +mkdir -p offline-package +cd offline-package + +# 下载 Docker 离线安装包 +yum install -y yum-utils +yumdownloader --resolve docker-ce docker-ce-cli containerd.io + +# 下载 Docker Compose 二进制文件 +wget https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64 +``` + +3. 创建部署脚本 + +```bash +# 打包所有文件 +tar czf docker-deploy-package.tar.gz \ + centos7-base.tar \ + collision-avoidance.tar \ + *.rpm \ + docker-compose-Linux-x86_64 \ + scripts/ +``` + +## 生产环境工作 + +### 1. 离线安装 Docker + +```bash +# 解压部署包 +tar xzf docker-deploy-package.tar.gz + +# 安装 Docker RPM 包 +rpm -ivh *.rpm + +# 安装 Docker Compose +cp docker-compose-Linux-x86_64 /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose + +# 启动 Docker 服务 +systemctl enable docker +systemctl start docker +``` + +### 2. 加载 Docker 镜像 + +```bash +# 加载基础镜像 +docker load < centos7-base.tar + +# 加载应用镜像 +docker load < collision-avoidance.tar +``` + +### 3. 部署配置 + +1. 创建数据目录 + +```bash +mkdir -p /data/collision-avoidance/{config,logs} +``` + +2. 配置文件准备 + +```bash +cp config/* /data/collision-avoidance/config/ +``` + +### 4. 启动���务 + +```bash +docker-compose up -d +``` + +## 部署后验证 + +### 1. 基础检查 + +- 容器状态 +- 端口可用性 +- 日志输出 +- 资源使用情况 + +### 2. 功能验证 + +- WebSocket 连接测试 +- Mock Server 功能测试 +- 碰撞检测功能测试 +- 数据持久化验证 + +### 3. 监控检查 + +- 健康检查状态 +- 资源监控指标 +- 日志收集状态 + +## 回滚方案 + +### 1. 准备回滚镜像 + +- 在测试环境保留上一个稳定版本镜像 +- 将回滚镜像包含在部署包中 + +### 2. 回滚步骤 + +```bash +# 停止当前服务 +docker-compose down + +# 加载回滚镜像 +docker load < collision-avoidance-rollback.tar + +# 修改镜像标签 +docker tag collision-avoidance-rollback:latest collision-avoidance:latest + +# 重启服务 +docker-compose up -d +``` + +## 注意事项 + +### 1. 安全考虑 + +- 使用非 root 用户运行容器 +- 配置文件权限控制 +- 网络访问限制 +- 资源限制设置 + +### 2. 性能优化 + +- 容器资源限制配置 +- 日志轮转策略 +- 数据持久化方案 + +### 3. 运维建议 + +- 定期备份配置文件 +- 监控告警配置 +- 日志管理策略 +- 定期检查磁盘使用情况 + +## 故障处理 + +### 1. 常见问题 + +- 容器启动失败 +- 端口冲突 +- 资源不足 +- 日志异常 + +### 2. 处理方法 + +- 检查容器日志 +- 验证配置文件 +- 检查��源使用 +- 查看系统日志 + +## 维护计划 + +### 1. 日常维护 + +- 日志清理 +- 磁盘空间检查 +- 性能监控 +- 配置备份 + +### 2. 定期更新 + +- 安全补丁更新 +- 基础镜像更新 +- 应用版本更新 diff --git a/docs/mock_server.md b/docs/mock_server.md new file mode 100644 index 0000000..77709a7 --- /dev/null +++ b/docs/mock_server.md @@ -0,0 +1,384 @@ +# Mock 服务设计与使用说明 + +## 1. 概述 + +Mock 服务用于模拟机场场面的移动物体(航空器和车辆),提供位置更新和响应控制指令的功能。服务包含两个主要部分: + +- HTTP API 服务:提供位置查询和车辆控制接口 +- 位置更新模拟:定期更新各个移动物体的位置 + +## 2. 移动物体配置 + +### 2.1 航空器配置 + +```python +{ + "flightNo": "AC001", # 航班号 + "latitude": 36.362647780875804, # 纬度 + "longitude": 120.088303, # 经度 + "direction": { # 使用方向向量 + "lat": initial_target_lat, + "lon": initial_target_lon + }, + "speed": 50.0 # 滑行速度 50km/h +} +``` + +### 2.2 车辆配置 + +```python +{ + "vehicleNo": "QN001", # 无人车1(西路口) + "latitude": WEST_INTERSECTION["latitude"], # 纬度 + "longitude": WEST_INTERSECTION["longitude"], # 经度 + "direction": 1, # 1表示向东,-1表示向西 + "speed": 36.0 # 行驶速度 36km/h +} +``` + +### 2.3 关键坐标点 + +- T1 (120.0868853, 36.35496367): 无人车1起点 +- T2 (120.08502054, 36.35448347): 冲突点1,西路口 +- T3 (120.08341044, 36.35406879): 特勤车终点 +- T4 (120.08558121, 36.35305878): 无人车1终点/无人车2起点/特勤车起点 +- T5 (120.08400957, 36.35265197) +- T6 (120.08649105, 36.35074527): 冲突点2,东路口 +- T7 (120.08562915, 36.35052372): 航空器1终点 +- T8 (120.08676664, 36.35004529): 无人车2终点 +- T9 (120.08520616, 36.34964473) +- T10 (120.08710569, 36.34917893) +- T11 (120.0873865, 36.3509885): 航空器1终点 +- T12 (120.08603613, 36.35190217): QN002新起点(距T6路口150米) + +## 3. 运动模式 + +### 3.1 航空器运动 + +- 沿跑道南北方向移动 +- 到达边界时自动调头 +- 保持恒定速度 50km/h + +### 3.2 无人车运动 + +- QN001: + 1. T1 -> T2(西路口) + 2. T2 -> T4 + 3. 到达T4后返回T1 + +- QN002: + 1. T12 -> T8 + 2. 到达T8后返回T12 + +- TQ001(特勤车): + 1. T4 -> T2(西路口) + 2. T2 -> T3 + 3. 到达T3后返回T4 + +### 3.3 配置参数 + +- 距离阈值: + - 300米:DIST_300M + - 200米:DIST_200M + - 150米:DIST_150M + - 100米:DIST_100M + - 50米:DIST_50M + +- 时间配置: + - 返回等待时间:10秒 + - 位置更新间隔:1秒 + - 红绿灯切换间隔:10秒 + +- 速度配置: + - 默认车辆速度:18.0 km/h + - 紧急制动减速度:80%/更新 + - 正常制动减速度:20%/更新 + +## 4. HTTP API 接口 + +### 4.1 航空器位置查询接口 + +```http +GET /api/getCurrentFlightPositions +``` + +返回所有移动物体的当前位置信息: + +```json +[ + { + "flightNo": "AC001", + "latitude": 36.362647780875804, + "longitude": 120.088303, + "direction": { + "lat": 0.707, + "lon": 0.707 + }, + "speed": 36.0, + "time": 1733741411167 + } +] +``` + +### 4.2 车辆位置查询接口 + +```http +GET /api/getCurrentVehiclePositions +``` + +返回所有车辆的当前位置信息: + +```json +[ + { + "vehicleNo": "QN001", + "latitude": 36.362448155990975, + "longitude": 120.08844920958369, + "direction": -1, + "speed": 18.0, + "time": 1733741411168, + "phase": 0 + } +] +``` + +### 4.3 红绿灯状态查询接口 + +```http +GET /api/getTrafficLightSignals +``` + +返回所有红绿灯的当前状态: + +```json +[ + { + "id": "TL001", + "state": 1, + "timestamp": 1733741411167, + "intersection": "INT001", + "position": { + "latitude": 36.35448347, + "longitude": 120.08502054 + } + } +] +``` + +### 4.4 车辆控制接口 + +```http +POST /api/vehicle/command +Content-Type: application/json +``` + +请求格式: + +```json +{ + "vehicleId": "QN001", + "type": "SIGNAL", // SIGNAL, ALERT, WARNING, RESUME + "reason": "TRAFFIC_LIGHT", // TRAFFIC_LIGHT, AIRCRAFT_CROSSING, SPECIAL_VEHICLE, AIRCRAFT_PUSH, RESUME_TRAFFIC + "timestamp": 1733741411167, + "signalState": "RED", // 可选,仅当 type 为 SIGNAL 时需要 + "targetPosition": { // 可选 + "x": 100.0, + "y": 200.0 + }, + "speed": 0.0, // 可选 + "distance": 0.0 // 可选 +} +``` + +响应格式: + +```json +{ + "status": "ok", // ok 或 error + "message": "Command executed successfully" +} +``` + +## 5. 指令优先级 + +车辆控制指令按以下优先级处理: + +1. ALERT (5):告警指令,最高优先级 + - 用于紧急情况,如碰撞风险 + - 可以覆盖所有其他指令 + - 收到后车辆立即停止 + +2. RED (4):红灯指令,次高优先级 + - 用于红绿灯控制 + - 可以覆盖预警、绿灯和恢复指令 + - 红灯时车辆停止,绿灯时可能恢复(取决于是否有更高优先级指令) + +3. WARNING (3):预警指令,中等优先级 + - 用于提前预警可能的风险 + - 可以覆盖绿灯和恢复指令 + - 收到后车辆减速或停止 + +4. GREEN (2):绿灯状态 + - 系统内部状态,不是外部指令 + - 可以覆盖恢复指令 + - 只有在没有更高优先级指令时才生效 + +5. RESUME (1):恢复指令,最低优先级 + - 用于恢复正常行驶 + - 不能覆盖其他指令 + - 只有在没有任何其他指令时才生效 + +### 5.1 优先级规则 + +1. 高优先级指令可以覆盖低优先级指令 +2. 相同优先级的新指令可以覆盖旧指令 +3. 低优先级指令不能覆盖高优先级指令 +4. 恢复行驶需要满足个条件: + - 收到 RESUME 指令 + - 没有任何更高优先级的指令 + +### 5.2 状态转换 + +1. 收到告警指令 (ALERT): + - 立即停止 + - 忽略所有低优先级指令 + - 只能通过 RESUME 指令恢复(且无其他高优先级指令) + +2. 收到红灯指令 (RED): + - 红灯:停止 + - 绿灯:如果没有更高优先级指令(ALERT)则恢复行驶 + +3. 收到预警指令 (WARNING): + - 减速或停止 + - 可被告警或红灯指令覆盖 + - 可通过绿灯或恢复指令解除(如无更高优先级指令) + +4. 遇到绿灯 (GREEN): + - 如果没有 ALERT、SIGNAL 或 WARNING 指令,则恢复行驶 + - 否则保持当前状态 + +5. 收到恢复指令 (RESUME): + - 检查是否有任何更高优先级指令 + - 如果没有,则恢复正常行驶速度 + - 如果有,则保持当前状态 + +### 5.3 使用示例 + +1. 告警指令覆盖其他指令: + + ```json + { + "vehicleId": "QN001", + "type": "ALERT", + "reason": "COLLISION_RISK", + "timestamp": 1733741411167 + } + ``` + +2. 红灯指令(但不会覆盖告警): + + ```json + { + "vehicleId": "QN001", + "type": "SIGNAL", + "reason": "TRAFFIC_LIGHT", + "signalState": "RED", + "timestamp": 1733741411167 + } + ``` + +3. 预警指令(可被告警和红灯覆盖): + + ```json + { + "vehicleId": "QN001", + "type": "WARNING", + "reason": "APPROACHING_INTERSECTION", + "timestamp": 1733741411167 + } + ``` + +4. 恢复指令(需要无更高优先级指令): + + ```json + { + "vehicleId": "QN001", + "type": "RESUME", + "reason": "RESUME_TRAFFIC", + "timestamp": 1733741411167 + } + ``` + +## 6. 使用示例 + +### 6.1 发送红绿灯指令 + +```bash +curl -X POST http://localhost:8081/api/vehicle/command \ + -H "Content-Type: application/json" \ + -d '{ + "vehicleId": "QN001", + "type": "SIGNAL", + "reason": "TRAFFIC_LIGHT", + "timestamp": 1733741411167, + "signalState": "RED" + }' +``` + +### 6.2 发送碰撞预警指令 + +```bash +curl -X POST http://localhost:8081/api/vehicle/command \ + -H "Content-Type: application/json" \ + -d '{ + "vehicleId": "QN001", + "type": "WARNING", + "reason": "AIRCRAFT_CROSSING", + "timestamp": 1733741411167, + "distance": 25.5 + }' +``` + +### 6.3 发送恢复指令 + +```bash +curl -X POST http://localhost:8081/api/vehicle/command \ + -H "Content-Type: application/json" \ + -d '{ + "vehicleId": "QN001", + "type": "RESUME", + "reason": "RESUME_TRAFFIC", + "timestamp": 1733741411167 + }' +``` + +## 7. 启动服务 + +```bash +# 安装依赖 +pip install flask + +# 启动服务 +python tools/mock_server.py +``` + +服务默认监听在: + +- HTTP 服务: + +## 8. 注意事项 + +1. 位置更新 + - 位置更新频率:1秒一次 + - 坐标系统:WGS84 + - 速度单位:km/h(内部计算使用 m/s) + +2. 车辆控制 + - 车辆收到停止指令后会立即停止位置更新 + - 只有收到 RESUME 指令且没有更高优先级的指令时才会恢复运动 + - 每个指令都必须包含时间戳 + +3. 错误处理 + - 无效的车辆ID:返回 404 错误 + - 无效的指令格式:返回 400 错误 + - 服务器内部错误:返回 500 错误 diff --git a/docs/official_api.md b/docs/official_api.md new file mode 100644 index 0000000..645c87f --- /dev/null +++ b/docs/official_api.md @@ -0,0 +1,225 @@ +# 数据接口对接方案 + +## 第1章 位置数据接口对接方案 + +### 1.1 登录认证 + +1. 登录接口: +2. 请求方式:post +3. 参数:username、password +4. 示例: +5、返回值 data 为返回的鉴权token,后续接口需要再header中携带,data所有的数据是一个token,不要截断 +示例:{ +    "status": 200, +    "msg": "登入成功", +    "data": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzI3ODMwOTAsInVzZXJuYW1lIjoiYWRtaW4ifQ.y9feEL_9NT8UzED9NNkb0Ln6C-PBoufiSHWobWe5vWY" +} + +### 1.2 航空器位置数据接入 + +数据来源:接入并转发从空管接收到的融合数据 + +1. 接口地址: + +2. 请求方式:get,需要在 Header 中携带认证信息,字段名为 Authorization,值为认证接口返回的token + +3. 返回格式:以 JSON 格式返回数据,一次请求返回List集合对象 + +4. 数据结构: + +| 序号 | 字段 | 描述 | 字段类型 | 是否必填 | +|-----|------|------|----------|----------| +| 1 | flightNo | 航班号 | String | 是 | +| 2 | longitude | 经度 | double | 是 | +| 3 | latitude | 纬度 | double | 是 | +| 4 | time | 时间戳(UTC 时间) | long | 是 | +| 5 | altitude | 海拔高度 | double | 否 | +| 6 | trackNumber | 航迹号 | long | 否 | + +### 1.3 车辆位置数据接入 + +数据来源:仅传递目前机场已接入的车辆位置数据 + +1. 接口地址: + +2. 请求方式:get,需要在 Header 中携带认证信息,字段名为 Authorization,值为认证接口返回的token + +3. 返回格式:以 JSON 格式返回数据,一次请求返回List集合对象 + +4. 数据结构: + +| 序号 | 字段 | 描述 | 字段类型 | 是否必填 | +|-----|------|------|----------|----------| +| 1 | vehicleNo | 车牌号 | String | 是 | +| 2 | longitude | 经度 | double | 是 | +| 3 | latitude | 纬度 | double | 是 | +| 4 | time | 时间戳 | long | 是 | +| 5 | direction | 方向 | double | 否 | +| 6 | speed | 速度 | double | 否 | + +## 第2章 无人车控制接口对接方案 + +### 2.1 无人车控制指令 + +2.1.1 接口地址: + +2.1.2 请求方法:POST + +2.1.3 请求参数: + +| 字段名称 | 类型 | 是否必填 | 说明 | +|---------|------|----------|------| +| transId | string | 是 | 消息唯一 id,消息的唯一标识符 | +| timestamp | long | 是 | 时间戳 | +| vehicleID | string | 是 | 车辆 ID | +| commandType | string | 是 | 指令类型:ALERT:告警指令,SIGNAL:信号灯指令,WARNING:预警指令,RESUME:恢复指令 | +| commandReason | string | 是 | 指令原因:TRAFFIC_LIGHT:红绿灯控制,AIRCRAFT_CROSSING:航空器交叉,SPECIAL_VEHICLE:特勤车辆,AIRCRAFT_PUSH:航空器推出,RESUME_TRAFFIC:恢复通行 | +| signalState | string | 否 | 信号灯状态(仅当 commandType 为 SIGNAL 时有效)RED:红灯,GREEN:绿灯,YELLOW:黄灯 | +| intersectionId | string | 否 | 路口 ID(仅当 commandType 为 SIGNAL 时有效) | +| latitude | double | 是 | 目标位置纬度(路口/航空器/特勤车) | +| longitude | double | 是 | 目标位置经度(路口/航空器/特勤车) | +| relativeSpeed | double | 否 | 相对速度(仅当 commandType 为 ALERT/WARNING 时有效) | +| relativeMotionX | double | 否 | 相对运动 X 分量(仅当 commandType 为 ALERT/WARNING 时有效) | +| relativeMotionY | double | 否 | 相对运动 Y 分量(仅当 commandType 为 ALERT/WARNING 时有效) | +| minDistance | double | 否 | 最小距离(仅当 commandType 为 ALERT/WARNING 时有效) | + +示例: + +requestData: +{ + "messageUniqueId": "68f79d1a-e27f-11ed-b28c-2cf05d9c2649", + "timestamp": 1736175610000, + "vehicleID": "A001", + "commandType": "SIGNAL", + "commandReason": "TRAFFIC_LIGHT", + "signalState":"RED", + "intersectionId":"002", + "latitude": 343.23, + "longitude": 343.23, + "relativeSpeed": 3, + "relativeMotionX": 2002.12, + "relativeMotionY":100.12, + "minDistance":10.5 +} + +返回值: + +| 字段名 | 类型 | 是否必须 | 描述 | +|---------|------|----------|------| +| transId | string | 是 | 消息唯一id,消息的唯一标识符与请求id一致 | +| timestamp | long | 是 | 时间戳 | +| code | int | 是 | 接口返回的状态码:200 请求成功:400 请求失败,并在msg内返回原因 | +| msg | string | 是 | 接口成功/失败的原因或者附加提示信息 | + +示例: + +responseData: +{ + "code": 200, + "msg": "success", + "transId": "68f79d1a-e27f-11ed-b28c-2cf05d9c2649", + "timestamp": 1736175610 +} + +### 2.2 无人车位置上报 + +1. 接口地址: + +2. 请求方法:GET + +3. 返回值(以 List 数据返回,一次请求返回集合对象): + +| 字段名称 | 类型 | 是否必填 | 说明 | +|---------|------|----------|------| +| transId | string | 是 | 消息唯一 id,消息的唯一标识符 | +| timestamp | long | 是 | 时间戳(UTC 时间,单位:毫秒) | +| vehicleID | string | 是 | 车辆 ID | +| latitude | double | 是 | 纬度 | +| longitude | double | 是 | 经度 | +| speed | double | 是 | 速度(单位:m/s) | +| direction | double | 是 | 车头航向角,正东为 0 度(弧度) | + +示例: + +requestData: +[ +{ + "transId": "68f79d1a-e27f-11ed-b28c-2cf05d9c2649", + "timestamp": 1736175610000, + "vehicleID": "AT001", + "latitude": 123.112, + "longitude": 78.331, + "speed": 3.2, + "direction": 1.57 +} +] + +### 2.3 无人车状态上报 + +1. 接口地址: + +2. 请求方法:POST + +3. 请求参数: + +| 字段名称 | 类型 | 是否必填 | 说明 | +|---------|------|----------|------| +| transId | string | 是 | 消息唯一 id,消息的唯一标识符 | +| timestamp | long | 是 | 时间戳(UTC 时间,单位:毫秒) | +| vehicleID | string | 是 | 车辆 ID | +| isSingle | boolean | 是 | True:单个车辆,False:所有车辆 | + +示例: + + requestData: + { + "transId": "68f79d1a-e27f-11ed-b28c-2cf05d9c2649", + "timestamp": 1736175610000, + "vehicleID": "AT001", + "isSingle": true + } + +4. 返回值(以 List 数据返回,一次请求返回集合对象): + +| 字段名称 | 类型 | 是否必填 | 说明 | +|---------|------|----------|------| +| transId | string | 是 | 消息唯一 id,消息的唯一标识符 | +| timestamp | long | 是 | 时间戳(UTC 时间,单位:毫秒) | +| vehicleID | string | 是 | 车辆 ID | +| loginState | boolean | 是 | 登录状态:True:登录,False:未登录 | +| faultInfo | list | 是 | 故障信息,以列表返回,可能存在多个 | +| activeSafety | boolean | 是 | 车辆最小风险策略触发(主动安全):True:触发,False:未触发 | +| RC | boolean | 是 | 被接管或干预相关信息,是否被远控RemoteControl,True:车辆在遥控器远控模式,False:车辆处于自动驾驶模式 | +| Command | int | 是 | 接收的远程指令信息,0:恢复,1:急停,2:缓停 | +| airportInfo | list | 否 | 机场特殊要求的其他信息 | +| vehicleMode | int | 是 | 无人设备控制模式(底盘控制模式),1:手动(司机驾驶),2:自动,3:遥控器,4:远程,5:故障等待 | +| gearState | int | 是 | 车辆当前档位,1:N,2:D,3:P,4:R, 5: 未知 | +| chassisReady | boolean | 是 | 底盘是否准备就绪,True:车辆发控制指令就可以走,false: 其他 | +| collisionStatus | boolean | 否 | 防撞梁是否触发,true:触发,false:未触发 | +| clearance | int | 是 | 0:关闭,1:开启(示廓灯) | +| turnSignalStstus | int | 是 | 转向灯状态,0:off , 1 : trun left , 2 : trun right, 3: 双闪 | +| pointCloud | list | 否 | 点云数据字节流,每个点的长度,现在是12,每个坐标为float,长度4| + +示例: + +responseData: +[ + { + "transId": "68f79d1a-e27f-11ed-b28c-2cf05d9c2649", + "timestamp": 1736175610000, + "vehicleID": "AT001", + "loginStatus":true, + "faultInfo":[], + "activeSafety":false, + "RC":false, + "Command":0, + "airportInfo":[], + "vehicleMode": 2, + "gearState": 2, + "chassisRaedy":true, + "collisionStatus":false, + "clearance":0, + "turnSignalStstus":0, + "pointCloud":[] + } +] diff --git a/docs/performance_test_report.md b/docs/performance_test_report.md new file mode 100644 index 0000000..2dc3092 --- /dev/null +++ b/docs/performance_test_report.md @@ -0,0 +1,340 @@ +# 碰撞检测系统性能测试报告 + +## 1. 测试环境 + +### 1.1 硬件环境 + +- CPU: Apple M3 Pro +- 内存: 16GB +- 存储: SSD + +### 1.2 软件环境 + +- 操作系统: macOS Sonoma 14.1 +- 编译器: Apple clang version 15.0.0 +- 编译选项: -O3 优化 +- Boost版本: 1.86.0 +- CMake版本: 3.14+ + +## 2. 测试场景 + +### 2.1 数据规模 + +- 航空器数量: 150架 +- 车辆数量: 300辆 +- 总物体数: 450个 + +### 2.2 空间分布 + +- 跑道区域: 5架航空器 +- 滑行道区域: 5架航空器 +- 停机位区域: 100架航空器,180辆车辆 +- 服务区域: 40架航空器,120辆车辆 + +### 2.3 区域范围 + +- 跑道区域: 3600m × 60m +- 滑行道区域: 3600m × 60m +- 停机位区域: 1500m × 1000m +- 服务区域: 2000m × 1000m + +## 3. 测试结果 + +### 3.1 性能指标 + +- 总处理时间: 4.987毫秒 +- 平均每个物体处理时间: 11微秒 +- 内存使用峰值: < 1MB + +### 3.2 碰撞检测结果 + +- 检测到的碰撞总数: 36个 +- 区域分布: + - 停机位区域: 30个 (83.3%) + - 服务区域: 4个 (11.1%) + - 滑行道区域: 2个 (5.6%) + - 跑道区域: 0个 + +### 3.3 风险等级分布 + +- 严重风险: 12个 (33.3%) +- 高风险: 14个 (38.9%) +- 中等风险: 8个 (22.2%) +- 低风险: 2个 (5.6%) + +### 3.4 空间优化效果 + +- 四叉树容量: 8个物体/节点 +- 机场边界: 4000m × 2000m +- 空间索引效率: O(log n) +- 总体复杂度: O(m * log n) + +## 4. 结论分析 + +### 4.1 性能表现 + +- 系统能够在5毫秒内完成453个物体的碰撞检测 +- 空间索引优化效果显著 +- 处理时间远低于100毫秒的实时性要求 +- 平均每个物体处理时间稳定在11微秒以内 + +### 4.2 检测结果 + +- 碰撞数量分布合理 +- 停机位区域碰撞占比83.3%,反映了实际情况 +- 服务区碰撞占比11.1%,空间利用合理 +- 滑行道碰撞占比5.6%,需要关注 +- 跑道无碰撞,符合安全要求 + +### 4.3 风险等级分析 + +- 严重风险和高风险占比72.2%,需要重点关注 +- 中等风险占比22.2%,在合理范围内 +- 低风险占比5.6%,说明预警及时 + +### 4.4 优化效果 + +- 区域尺寸调整后,碰撞检测更准确 +- 物体分布更接近实际情况 +- 风险等级划分合理 +- 处理时间稳定可控 + +## 5. 边界条件测试 + +### 5.1 距离边界 + +- 最小检测距离: 7.5米(车辆碰撞半径的一半) +- 最大检测距离: 100米(跑道区域航空器阈值) +- 临界距离测试: 通过 + +### 5.2 速度边界 + +- 静止物体: 正确处理 +- 最大速度: 70 m/s(航空器) +- 相对静止: 正确识别 + +### 5.3 空间边界 + +- 区域边界重叠: 正确处理 +- 区域切换: 平滑过渡 +- 机场边界: 正确约束 + +## 6. 性能瓶颈分析 + +### 6.1 主要耗时 + +1. 四叉树更新: 15% +2. 空间查询: 25% +3. 距离计算: 35% +4. 相对运动分析: 25% + +### 6.2 优化空间 + +1. 四叉树容量参数调优 +2. 并行处理大规模数据 +3. SIMD指令优化距离计算 +4. 缓存历史碰撞结果 + +## 7. 结论与建议 + +### 7.1 性能结论 + +- 系统满足实时性要求(<10ms) +- 内存使用合理 +- 算法复杂度符合预期 +- 空间索引效果显著 + +### 7.2 改进建议 + +1. 引入多线程处理 +2. 优化四叉树参数 +3. 添加预测性碰撞检测 +4. 实现增量更新机制 +5. 添加碰撞风险等级分类 + +### 7.3 后续测试建议 + +1. 进行持续运行测试(>24小时) +2. 测试极端天气条件下的表现 +3. 测试高并发场景 +4. 进行内存泄漏测试 +5. 进行载均衡测试 + +## 8. 附录 + +### 8.1 测试代码片段 + +```cpp +// 大规模碰撞检测性能测试 +TEST_F(CollisionDetectorTest, LargeScaleCollisionDetection) { + std::srand(std::time(nullptr)); + + // 生成100架航空器 + std::vector aircraft; + for (int i = 0; i < 100; ++i) { + Aircraft a; + a.id = "FL" + std::to_string(i + 1); + a.position = generateRandomPosition(); + a.speed = generateRandomSpeed(true); + a.heading = generateRandomHeading(); + aircraft.push_back(a); + } + + // 生成200辆车 + std::vector vehicles; + for (int i = 0; i < 200; ++i) { + Vehicle v; + v.id = "VH" + std::to_string(i + 1); + v.position = generateRandomPosition(); + v.speed = generateRandomSpeed(false); + v.heading = generateRandomHeading(); + vehicles.push_back(v); + } + + auto start = std::chrono::high_resolution_clock::now(); + detector->updateTraffic(aircraft, vehicles); + auto collisions = detector->detectCollisions(); + auto duration = std::chrono::duration_cast + (std::chrono::high_resolution_clock::now() - start); + + // 验证结果... +} +``` + +### 8.2 测试数据分布 + +- 位置分布: 均匀随机 +- 速度分布: 线性随机 +- 向分布: 均匀随机 +- 碰撞分布: 自然随机 + +## 9. 碰撞检测详细配置 + +### 9.1 告警阈值配置 + +#### 9.1.1 直接告警阈值(距离小于阈值的一半) + +- 航空器与车辆: + - 跑道:50米 + - 滑行道:25米 + - 停机位:20米 + - 服务区:15米 + +- 车辆之间: + - 跑道:25米 + - 滑行道:15米 + - 停机位:10米 + - 服务区:7.5米 + +#### 9.1.2 相对运动告警阈值 + +- 航空器与车辆: + - 跑道:100米 + - 滑行道:50米 + - 停机位:40米 + - 服务区:30米 + +- 车辆之间: + - 跑道:50米 + - 滑行道:30米 + - 停机位:20米 + - 服务区:15米 + +### 9.2 算法实现细节 + +#### 9.2.1 空间索引实现 + +```cpp +class QuadTree { + Bounds bounds_; // 区域边界 + int capacity_; // 节点容量(8个物体/节点) + std::vector items_; // 物体列表 + bool divided_ = false; // 是否已分割 + + std::unique_ptr northwest_; // 四个子节点 + std::unique_ptr northeast_; + std::unique_ptr southwest_; + std::unique_ptr southeast_; +}; +``` + +#### 9.2.2 碰撞检测实现 + +```cpp +bool CollisionDetector::checkAircraftVehicleCollision(const Aircraft& aircraft, + const Vehicle& vehicle) const { + const auto& areaConfig = getCollisionParams(aircraft.position); + + // 计算平面距离 + double dx = aircraft.position.x - vehicle.position.x; + double dy = aircraft.position.y - vehicle.position.y; + double distance = std::sqrt(dx*dx + dy*dy); + + // 直接告警条件 + if (distance < areaConfig.aircraftGroundRadius * 0.5) { + return true; + } + + // 相对运动检测 + if (distance < areaConfig.aircraftGroundRadius) { + // 计算相对速度 + double vax = aircraft.speed * std::cos((90 - aircraft.heading) * M_PI / 180.0); + double vay = aircraft.speed * std::sin((90 - aircraft.heading) * M_PI / 180.0); + double vvx = vehicle.speed * std::cos((90 - vehicle.heading) * M_PI / 180.0); + double vvy = vehicle.speed * std::sin((90 - vehicle.heading) * M_PI / 180.0); + double vx = vax - vvx; + double vy = vay - vvy; + + // 计算相对运动 + double relativeMotion = dx*vx + dy*vy; + return relativeMotion < 0; // 物体正在接近 + } + + return false; +} +``` + +### 9.3 性能优化策略 + +#### 9.3.1 空间查询优化 + +1. 四叉树空间分区: + - 将机场空间划分为四个子区域 + - 递归分割直到达到容量限制 + - 快速定位邻近物体 + +2. 邻近查询实现: + +```cpp +std::vector queryNearby(const Vector2D& point, double radius) const { + Bounds range{ + point.x - radius, + point.y - radius, + radius * 2, + radius * 2 + }; + return queryRange(range); +} +``` + +#### 9.3.2 计算优化 + +1. 距离计算: + - 使用平方距离比较,避免开方运算 + - 只在必要时计算精确距离 + +2. 相对运动计算: + - 使用航向角的正弦和余弦值 + - 考虑数学坐标系的转换 + +### 9.4 内存优化 + +1. 数据结构: + - 使用智能指针管理四叉树节点 + - 避免不必要的数据复制 + - 合理设置容器初始容量 + +2. 缓存策略: + - 缓存航空器数据 + - 使用四叉树索引车辆数据 + - 及时清理无效数据 diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..2534900 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,60 @@ +# 需求文档 + +## 1. 需求概述 + +## 2. 功能需求 + +### 2.1 航空器、车辆位置显示 + +### 2.2 冲突检测 + + 1. 路径检测 + 2. 距离检测 + 3. 路口检测 + +### 2.3 无人车辆控制 + + 1. 无人车控制 + 1. 控制指令 + 1. 冲突预警 + 2. 冲突告警 + 3. 安全停靠 + 2. + +### 2.4 红绿灯信号处理 + +### 2.5 数据采集 + +1. 航空器和车辆位置数据 + 1. 数据来源: + 1. 航空器:ADS-B + 2. 车辆:1.8G北斗 + 2. 数据格式: + 参考 official_api.md + 3. 采集频率: + 1. 航空器:3Hz + 2. 车辆:3Hz +2. 红绿灯信号 + 1. 数据来源: + 1. 红绿灯:5G + 2. 数据格式: + 参考 official_api.md + 3. 采集频率: + 1. 红绿灯:1Hz +3. 无人车状态 + 1. 数据来源: + 1. 无人车:5G + 2. 数据格式: + 参考 official_api.md + 3. 采集频率: + 1. 无人车:1Hz + +### 2.5 数据处理 + +### 2.6 数据存储 + +## 3. 非功能需求 + +## 4. 约束 + +## 5. 其他 diff --git a/docs/warning_strategy.md b/docs/warning_strategy.md new file mode 100644 index 0000000..717b3b8 --- /dev/null +++ b/docs/warning_strategy.md @@ -0,0 +1,203 @@ +# 机场场面告警策略文档 + +## 1. 控制指令定义 + +系统对无人车的控制分为以下指令类型: + +```cpp +// 指令类型 +enum class CommandType { + ALERT, // 告警指令 + SIGNAL, // 信号灯指令 + WARNING, // 预警指令 + RESUME // 恢复指令 +}; + +// 指令原因 +enum class CommandReason { + TRAFFIC_LIGHT, // 红绿灯控制 + AIRCRAFT_CROSSING, // 航空器交叉 + SPECIAL_VEHICLE, // 特勤车辆 + AIRCRAFT_PUSH, // 航空器推出 + RESUME_TRAFFIC // 恢复通行 +}; + +// 信号灯状态 +enum class SignalState { + RED, // 红灯 + GREEN // 绿灯 +}; +``` + +### 1.1 信号灯指令(SIGNAL) + +- **目的**:响应交通信号灯 +- **触发条件**:接收到红绿灯状态变化 +- **执行动作**:根据信号灯状态停车或通行 +- **优先级**:4(最高优先级) +- **原因**:TRAFFIC_LIGHT + +### 1.2 告警指令(ALERT) + +- **目的**:应对紧急情况 +- **触发条件**:检测到直接碰撞风险 +- **执行动作**:强制停车或紧急避让 +- **优先级**:3 +- **原因**:AIRCRAFT_CROSSING, SPECIAL_VEHICLE, AIRCRAFT_PUSH + +### 1.3 预警指令(WARNING) + +- **目的**:提前告知可能的风险 +- **触发条件**:进入监测区域但尚未到达关键区域 +- **执行动作**:降低速度,提高警惕 +- **优先级**:2 +- **原因**:AIRCRAFT_CROSSING, SPECIAL_VEHICLE, AIRCRAFT_PUSH + +### 1.4 恢复指令(RESUME) + +- **目的**:恢复到正常行驶状态 +- **触发条件**:航空器或特勤车辆通过后,距离路口 50 m后 +- **执行动作**:恢复到正常行驶状态 +- **优先级**:1(最低优先级) +- **原因**:RESUME_TRAFFIC + +## 2. 红绿灯条件下的告警策略 + +### 2.1 适用场景 + +- 无人车接近带有红绿灯的交叉路口时的行为控制 +- 主要针对机场内各主要道路交叉口 + +### 2.2 告警条件 + +1. **距离阈值**: + - 车辆进入路口 200 米范围内开始监测 + - 在距离路口 50 米处为停车点 + +2. **信号灯状态响应**: + + ```cpp + struct VehicleCommand { + std::string vehicleId; // 车辆ID + CommandType type; // 指令类型(SIGNAL) + CommandReason reason; // 指令原因(TRAFFIC_LIGHT) + SignalState signalState; // 信号灯状态 + std::string intersectionId; // 路口ID + double latitude; // 路口纬度 + double longitude; // 路口经度 + }; + ``` + +## 3. 航空器或特勤车辆,和无人车垂直经过交叉路口的告警策略 + +### 3.1 适用场景 + +- 滑行道与车行道的交叉路口 +- 航空器或特勤车辆,和无人车垂直交叉,预判可能冲突的情况 + +### 3.2 告警条件 + +1. **距离监测**: + - 航空器或特勤车辆通过路口,无人车距离路口阈值:100 米 + +2. **控制策略**: + + ```cpp + struct VehicleCommand { + std::string vehicleId; // 车辆ID + CommandType type; // 指令类型(WARNING/ALERT) + CommandReason reason; // 指令原因(AIRCRAFT_CROSSING/SPECIAL_VEHICLE) + double latitude; // 目标位置纬度 + double longitude; // 目标位置经度 + double relativeSpeed; // 相对速度 + double relativeMotionX; // 相对运动 X 分量 + double relativeMotionY; // 相对运动 Y 分量 + double minDistance; // 最小距离 + }; + ``` + +## 4. 航空器出库时的告警策略 + +### 4.1 适用场景 + +- 航空器从机库或维修区驶出 +- 与服务区道路的交叉路口 + +### 4.2 告警条件 + +1. **距离监测**: + - 航空器通过路口,无人车距离路口阈值:100 米 + +2. **控制策略**: + + ```cpp + struct VehicleCommand { + std::string vehicleId; // 车辆ID + CommandType type; // 指令类型(WARNING/ALERT) + CommandReason reason; // 指令原因(AIRCRAFT_PUSH) + double latitude; // 目标位置纬度 + double longitude; // 目标位置经度 + double relativeSpeed; // 相对速度 + double relativeMotionX; // 相对运动 X 分量 + double relativeMotionY; // 相对运动 Y 分量 + double minDistance; // 最小距离 + }; + ``` + +## 5. 通用要求 + +### 5.1 指令优先级处理 + +1. SIGNAL 指令(优先级 4) + - 立即执行 + - 覆盖其他所有指令 + - 红绿灯控制具有最高优先级,确保交通安全 + +2. ALERT 指令(优先级 3) + - 可被 SIGNAL 指令覆盖 + - 优先于 WARNING 指令 + - 用于碰撞风险等紧急情况 + +3. WARNING 指令(优先级 2) + - 可被 SIGNAL 和 ALERT 指令覆盖 + - 用于预防性控制 + - 在无更高优先级指令时执行 + +4. RESUME 指令(优先级 1) + - 可被其他指令覆盖 + - 用于恢复到正常行驶状态 + +### 5.2 系统响应时间 + +- 告警信号处理延迟 < 100ms +- 控制指令执行延迟 < 200ms +- 状态反馈延迟 < 300ms + +### 5.3 降级处理 + +- 在通信中断时,无人车应自动降速或停车 +- 在传感器失效时,扩大安全距离 + +### 5.4 提前量 + +- 无人车在距离路口 110 米处开始监测,提前 10 米发送 WARNING 指令 +- 无人车在距离路口 60 米处开始监测,提前 10 米发送 ALERT 指令 + +### 5.5 指令格式 + +```cpp +struct VehicleCommand { + std::string vehicleId; // 车辆ID + CommandType type; // 指令类型 + CommandReason reason; // 指令原因 + uint64_t timestamp; // 时间戳 + SignalState signalState; // 信号灯状态(仅当 type 为 SIGNAL 时有效) + std::string intersectionId; // 路口ID(仅当 type 为 SIGNAL 时有效) + double latitude; // 目标位置纬度(路口/航空器/特勤车) + double longitude; // 目标位置经度(路口/航空器/特勤车) + double relativeSpeed; // 相对速度(仅当 type 为 ALERT/WARNING 时有效) + double relativeMotionX; // 相对运动 X 分量(仅当 type 为 ALERT/WARNING 时有效) + double relativeMotionY; // 相对运动 Y 分量(仅当 type 为 ALERT/WARNING 时有效) + double minDistance; // 最小距离(仅当 type 为 ALERT/WARNING 时有效) +}; +``` diff --git a/scripts/build_execute_package.sh b/scripts/build_execute_package.sh new file mode 100644 index 0000000..b7f665b --- /dev/null +++ b/scripts/build_execute_package.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# Color definitions for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Get script directory and project root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")" +PROJECT_NAME="collision_avoidance" + +# Generate version number based on current time +VERSION=$(date +"%Y%m%d_%H%M%S") +log_info "Building version: ${VERSION}" + +# Create temp directory +TEMP_DIR="${PROJECT_ROOT}/temp_build" +rm -rf "${TEMP_DIR}" +mkdir -p "${TEMP_DIR}/bin" +mkdir -p "${TEMP_DIR}/lib" + +# Build the project +log_info "Building project..." +cd "${PROJECT_ROOT}" +rm -rf build +mkdir build +cd build + +# Configure and build +cmake3 -DCMAKE_BUILD_TYPE=Release .. +if [ $? -ne 0 ]; then + log_error "CMake configuration failed" + exit 1 +fi + +make -j$(nproc) +if [ $? -ne 0 ]; then + log_error "Build failed" + exit 1 +fi + +# Copy executable +log_info "Copying executable..." +cp "bin/${PROJECT_NAME}" "${TEMP_DIR}/bin/" || { + log_error "Failed to copy executable" + exit 1 +} + +# --- BEGIN: Copy Libraries --- +log_info "Copying required runtime libraries to ${TEMP_DIR}/lib/ ..." + +GCC_RUNTIME_LIB_PATH="/usr/local/lib64" +if [ -f "${GCC_RUNTIME_LIB_PATH}/libstdc++.so.6" ]; then + cp -Lv "${GCC_RUNTIME_LIB_PATH}/libstdc++.so.6" "${TEMP_DIR}/lib/" + log_info "Copied libstdc++.so.6 (and its target file)" +else + log_warn "libstdc++.so.6 not found in ${GCC_RUNTIME_LIB_PATH}" +fi + +if [ -f "${GCC_RUNTIME_LIB_PATH}/libgcc_s.so.1" ]; then + cp -Lv "${GCC_RUNTIME_LIB_PATH}/libgcc_s.so.1" "${TEMP_DIR}/lib/" + log_info "Copied libgcc_s.so.1 (and its target file)" +else + log_warn "libgcc_s.so.1 not found in ${GCC_RUNTIME_LIB_PATH}" +fi + +# Modified Boost library copying +ACTUAL_BOOST_LIB_PATH="/usr/lib64" # Actual .so.1.69.0 files are in /usr/lib64 +BOOST_VERSIONED_FILES=( + "libboost_system.so.1.69.0" + "libboost_filesystem.so.1.69.0" + "libboost_thread.so.1.69.0" + "libboost_chrono.so.1.69.0" + "libboost_date_time.so.1.69.0" + "libboost_atomic.so.1.69.0" + "libboost_regex.so.1.69.0" +) + +log_info "Attempting to copy Boost libraries from ${ACTUAL_BOOST_LIB_PATH}..." +for boost_file in "${BOOST_VERSIONED_FILES[@]}"; do + if [ -f "${ACTUAL_BOOST_LIB_PATH}/${boost_file}" ]; then + cp -v "${ACTUAL_BOOST_LIB_PATH}/${boost_file}" "${TEMP_DIR}/lib/" + log_info "Copied ${boost_file}" + else + log_warn "Boost library ${boost_file} not found in ${ACTUAL_BOOST_LIB_PATH}" + fi +done +# --- END: Copy Libraries --- + +# Generate version file +echo "${VERSION}" > "${TEMP_DIR}/bin/version.txt" + +# Generate checksum +cd "${TEMP_DIR}/bin" +sha256sum "${PROJECT_NAME}" > "${PROJECT_NAME}.sha256" + +# Create deployment archive +cd "${TEMP_DIR}" +PACKAGE_NAME="${PROJECT_NAME}_${VERSION}.tar.gz" +tar czf "${PROJECT_ROOT}/${PACKAGE_NAME}" bin/ lib/ + +# Cleanup +cd "${PROJECT_ROOT}" +rm -rf "${TEMP_DIR}" + +log_info "Package created: ${PACKAGE_NAME}" +log_info "Package contents:" +log_info " - Executable binary (in bin/)" +log_info " - Runtime libraries (in lib/)" +log_info " - Version file (in bin/)" +log_info " - Checksum file (in bin/)" + +# Print instructions +echo +log_info "To deploy:" +echo "1. Copy ${PACKAGE_NAME} to production environment" +echo "2. Extract: tar xzf ${PACKAGE_NAME}" +echo " This will create 'bin/' and 'lib/' directories." +echo "3. Verify checksum: cd bin && sha256sum -c ${PROJECT_NAME}.sha256 && cd .." +echo "4. Stop service: systemctl stop collision-avoidance" +echo "5. Backup old binary and libraries (optional but recommended)" +echo " e.g., mv /opt/collision_avoidance/bin/${PROJECT_NAME} /opt/collision_avoidance/bin/${PROJECT_NAME}.bak" +echo " e.g., rsync -ab /opt/collision_avoidance/lib/ /opt/collision_avoidance/lib_bak/" +echo "6. Copy new binary: cp bin/${PROJECT_NAME} /opt/collision_avoidance/bin/" +echo "7. Copy new libraries: cp lib/* /opt/collision_avoidance/lib/" +echo "8. Start service: systemctl start collision-avoidance" +echo "9. Check status: systemctl status collision-avoidance" \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..2ef145d --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,256 @@ +#!/bin/bash + +# Set up logging +LOGFILE="/tmp/deploy.log" + +# Color definitions for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" + echo "[INFO] $1" >> "$LOGFILE" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" + echo "[WARN] $1" >> "$LOGFILE" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" + echo "[ERROR] $1" >> "$LOGFILE" +} + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + log_error "Please run as root" + exit 1 +fi + +# Get script directory and project root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")" + +# Install RPM packages +log_info "Installing RPM packages..." +if [ ! -d "${PROJECT_ROOT}/rpm" ]; then + log_error "RPM directory not found: ${PROJECT_ROOT}/rpm" + exit 1 +fi +cd "${PROJECT_ROOT}/rpm" + +# Install Python3 and its dependencies +if ! ls python3*.rpm >/dev/null 2>&1; then + log_error "Python3 RPM packages not found" + exit 1 +fi +rpm -Uvh --nodeps python3*.rpm + +# Install Python packages from wheel files +log_info "Installing Python packages..." +if [ ! -d "${PROJECT_ROOT}/python" ]; then + log_error "Python packages directory not found: ${PROJECT_ROOT}/python" + exit 1 +fi + +# Install wheel files in correct order +log_info "Installing Python packages..." +cd "${PROJECT_ROOT}/python" +for pkg in *.whl; do + if [ ! -f "$pkg" ]; then + log_error "Required package not found: $pkg" + exit 1 + fi + log_info "Installing $pkg..." + pip3 install --no-index "$pkg" +done + +# Verify Python packages +log_info "Verifying Python packages..." +python3 -c "import flask; import werkzeug; import jinja2; import markupsafe" || { + log_error "Failed to verify Python packages" + exit 1 +} + +# Create work directory +WORK_DIR="/opt/collision_avoidance" +log_info "Creating work directory: $WORK_DIR" +mkdir -p $WORK_DIR +mkdir -p $WORK_DIR/bin +mkdir -p $WORK_DIR/lib +mkdir -p $WORK_DIR/tools +mkdir -p $WORK_DIR/config +mkdir -p $WORK_DIR/logs + +# Set directory permissions +log_info "Setting directory permissions..." +chmod 755 $WORK_DIR +chmod 755 $WORK_DIR/bin +chmod 755 $WORK_DIR/lib +chmod 755 $WORK_DIR/tools +chmod 755 $WORK_DIR/config +chmod 755 $WORK_DIR/logs +chown -R root:root $WORK_DIR + +# Copy files +log_info "Copying files..." + +# Check SELinux status +log_info "Checking SELinux status..." +if command -v getenforce &> /dev/null; then + selinux_status=$(getenforce) + log_info "SELinux status: $selinux_status" + if [ "$selinux_status" = "Enforcing" ]; then + log_info "Setting SELinux contexts..." + + # 设置用目录的 SELinux 上下文 + log_info "Setting context for application directories..." + chcon -R -t usr_t $WORK_DIR + chcon -t bin_t $WORK_DIR/bin + chcon -t lib_t $WORK_DIR/lib + chcon -t etc_t $WORK_DIR/config + + # 恢复默认上下文 + log_info "Restoring default contexts..." + restorecon -Rv $WORK_DIR + fi +else + log_warn "SELinux tools not found" +fi + +for dir in bin lib python tools config; do + if [ ! -d "${PROJECT_ROOT}/${dir}" ]; then + log_error "Directory not found: ${PROJECT_ROOT}/${dir}" + exit 1 + fi +done + +# Copy executable +log_info "Copying executable..." +if [ "$EXECUTABLE_COPIED" != "1" ]; then + if ! cp -v "${PROJECT_ROOT}/bin/"* "$WORK_DIR/bin/"; then + log_error "Failed to copy executable" + exit 1 + fi +fi + +# Copy libraries +log_info "Copying libraries..." +if ! cp -v "${PROJECT_ROOT}/lib/"* "$WORK_DIR/lib/"; then + log_error "Failed to copy libraries" + exit 1 +fi + +# Copy tools +log_info "Copying tools..." +if ! cp -v "${PROJECT_ROOT}/tools/"* "$WORK_DIR/tools/"; then + log_error "Failed to copy tools" + exit 1 +fi + +# Create config directory +CONFIG_DIR="$WORK_DIR/config" +log_info "Creating config directory: $CONFIG_DIR" +mkdir -p $CONFIG_DIR +log_info "Copying configuration files..." +for config_file in system_config.json vehicle_control.json airport_bounds.json intersections.json; do + if [ ! -f "${PROJECT_ROOT}/config/$config_file" ]; then + log_error "Config file not found: ${PROJECT_ROOT}/config/$config_file" + exit 1 + fi + if ! cp -v "${PROJECT_ROOT}/config/$config_file" "$CONFIG_DIR/"; then + log_error "Failed to copy config file: $config_file" + exit 1 + fi +done + +# 设置配置文件权限 +log_info "Setting config file permissions..." +chmod 644 "$CONFIG_DIR"/*.json +chown -R root:root "$CONFIG_DIR" + +# 验证配置文件 +log_info "Verifying config files..." +if ! ls -l "$CONFIG_DIR"/*.json; then + log_error "No config files found in $CONFIG_DIR" + exit 1 +fi + +# Create service file +log_info "Creating system service..." +cat > /etc/systemd/system/collision-avoidance.service << EOL +[Unit] +Description=Collision Avoidance Service +After=network.target + +[Service] +Type=simple +ExecStart=$WORK_DIR/bin/collision_avoidance +WorkingDirectory=$WORK_DIR +User=root +Restart=always +RestartSec=3 +Environment=CONFIG_PATH=$WORK_DIR/config +Environment=LD_LIBRARY_PATH=$WORK_DIR/lib +StandardOutput=append:$WORK_DIR/logs/collision-avoidance.log +StandardError=append:$WORK_DIR/logs/collision-avoidance.error.log + +[Install] +WantedBy=multi-user.target +EOL + +# Configure firewall +log_info "Configuring firewall..." +if command -v firewall-cmd &> /dev/null; then + if ! systemctl is-active --quiet firewalld; then + log_info "Starting firewalld service..." + systemctl start firewalld + fi + firewall-cmd --permanent --add-port=8010/tcp + firewall-cmd --permanent --add-port=8081/tcp + firewall-cmd --reload +else + log_warn "firewalld not detected, please configure firewall manually" +fi + +# Start service +log_info "Starting service..." +systemctl daemon-reload +systemctl enable collision-avoidance +systemctl start collision-avoidance + +# Check service status +if systemctl is-active --quiet collision-avoidance; then + log_info "Service started successfully" +else + log_error "Service failed to start, check logs" + journalctl -u collision-avoidance -n 50 + exit 1 +fi + +# Display service status +log_info "Deployment completed, service status:" +systemctl status collision-avoidance + +# Display usage instructions +echo -e "\n${GREEN}Deployment completed!${NC}" +echo "Use these commands to manage the service:" +echo " Start service: systemctl start collision-avoidance" +echo " Stop service: systemctl stop collision-avoidance" +echo " Restart service: systemctl restart collision-avoidance" +echo " Check status: systemctl status collision-avoidance" +echo " View logs: journalctl -u collision-avoidance -f" +echo -e "\nConfig files location: ${CONFIG_DIR}" +echo "WebSocket port: 8010" +echo "Mock Server port: 8081" + +# 创建日志文件 +log_info "Creating log files..." +touch $WORK_DIR/logs/collision-avoidance.log +touch $WORK_DIR/logs/collision-avoidance.error.log +chmod 644 $WORK_DIR/logs/*.log +chown root:root $WORK_DIR/logs/*.log \ No newline at end of file diff --git a/scripts/prepare_deploy.sh b/scripts/prepare_deploy.sh new file mode 100644 index 0000000..23c1628 --- /dev/null +++ b/scripts/prepare_deploy.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# Color definitions for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Get script directory and project root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")" +PROJECT_NAME="$(basename "${PROJECT_ROOT}")" + +# Create deploy directory structure +DEPLOY_DIR="${PROJECT_ROOT}/deploy" +log_info "Creating deploy directory: ${DEPLOY_DIR}" +rm -rf "${DEPLOY_DIR}" +mkdir -p "${DEPLOY_DIR}/bin" +mkdir -p "${DEPLOY_DIR}/lib" +mkdir -p "${DEPLOY_DIR}/python" +mkdir -p "${DEPLOY_DIR}/config" +mkdir -p "${DEPLOY_DIR}/rpm" +mkdir -p "${DEPLOY_DIR}/tools" +mkdir -p "${DEPLOY_DIR}/scripts" + +# Download Python3 RPM packages +log_info "Downloading Python3 RPM packages..." +cd "${DEPLOY_DIR}/rpm" + +# Install required tools +log_info "Installing required tools..." +if ! command -v repotrack &> /dev/null; then + sudo yum install -y yum-utils +fi + +# Install build dependencies +log_info "Installing build dependencies..." +sudo yum groupinstall -y "Development Tools" +sudo yum install -y epel-release +sudo yum install -y cmake3 boost-devel openssl-devel python3-devel libcurl-devel + +# Install nlohmann-json from EPEL +log_info "Installing nlohmann-json..." +sudo yum install -y nlohmann-json-devel + +# Install GCC 11 +log_info "Installing GCC 11..." +sudo yum install -y centos-release-scl +sudo yum install -y devtoolset-11-gcc devtoolset-11-gcc-c++ + +# Enable GCC 11 +log_info "Enabling GCC 11..." +source /opt/rh/devtoolset-11/enable + +# Download Python3 and dependencies +log_info "Downloading Python3 and dependencies..." +repotrack python3 python3-pip python3-devel python3-flask python3-werkzeug python3-jinja2 + +# Build the project +log_info "Building project..." +cd "${PROJECT_ROOT}" +rm -rf build +mkdir build +cd build +cmake3 .. +make -j4 + +if [ $? -ne 0 ]; then + log_error "Build failed" + exit 1 +fi + +# Copy executable +log_info "Copying executable..." +cp "bin/collision_avoidance" "${DEPLOY_DIR}/bin/" || { + log_error "Failed to copy executable" + exit 1 +} + +# Copy required libraries +log_info "Copying required libraries..." +LIBS=( + "/usr/lib64/libboost_system.so.1.69.0" + "/usr/lib64/libboost_filesystem.so.1.69.0" + "/usr/lib64/libboost_thread.so.1.69.0" + "/usr/lib64/libssl.so.1.0.2k" + "/usr/lib64/libcrypto.so.1.0.2k" +) + +for lib in "${LIBS[@]}"; do + if [[ "$lib" == /* ]]; then + # 对于完整路径的库文件,直接复制 + if [ -f "$lib" ]; then + cp "$lib" "${DEPLOY_DIR}/lib/" + log_info "Copied $(basename $lib)" + else + log_warn "Library $lib not found" + fi + else + # 对于其他库文件,使用 ldd 查找 + lib_path=$(ldd "${DEPLOY_DIR}/bin/collision_avoidance" | grep "$lib" | awk '{print $3}') + if [ -n "$lib_path" ]; then + cp "$lib_path" "${DEPLOY_DIR}/lib/" + log_info "Copied $lib" + else + log_warn "Library $lib not found" + fi + fi +done + +# Copy Python dependencies +log_info "Copying Python dependencies..." +mkdir -p "${DEPLOY_DIR}/python" +cd "${DEPLOY_DIR}/python" + +# 下载所有依赖包及其依赖 +pip3 download \ + flask==2.0.1 werkzeug==2.0.1 click==8.0.1 \ + itsdangerous==2.0.1 Jinja2==3.0.1 MarkupSafe==2.0.1 + +# Copy configuration files +log_info "Copying configuration files..." +cp "${PROJECT_ROOT}/config/airport_bounds.json" "${DEPLOY_DIR}/config/" +cp "${PROJECT_ROOT}/config/intersections.json" "${DEPLOY_DIR}/config/" +cp "${PROJECT_ROOT}/config/system_config.json" "${DEPLOY_DIR}/config/" +cp "${PROJECT_ROOT}/config/vehicle_control.json" "${DEPLOY_DIR}/config/" + +# Copy tools +log_info "Copying tools..." +cp "${PROJECT_ROOT}/tools/mock_server.py" "${DEPLOY_DIR}/tools/" +cp "${PROJECT_ROOT}/tools/map_websocket.html" "${DEPLOY_DIR}/tools/" +cp "${PROJECT_ROOT}/tools/test_websocket.html" "${DEPLOY_DIR}/tools/" + +# Copy deployment scripts +log_info "Copying deployment scripts..." +cp "${PROJECT_ROOT}/scripts/deploy.sh" "${DEPLOY_DIR}/scripts/" +cp "${PROJECT_ROOT}/scripts/uninstall.sh" "${DEPLOY_DIR}/scripts/" +chmod +x "${DEPLOY_DIR}/scripts/"*.sh + +# Create deployment archive +log_info "Creating deployment archive..." +cd "${PROJECT_ROOT}" +tar czf "${PROJECT_NAME}_deploy.tar.gz" deploy/ + +log_info "Deployment package created: ${PROJECT_NAME}_deploy.tar.gz" +echo "Package contents:" +echo " - Executable binary" +echo " - Required runtime libraries" +echo " - Python3 RPM packages" +echo " - Python dependencies" +echo " - Configuration files" +echo " - Tools" +echo " - Deployment scripts" \ No newline at end of file diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100644 index 0000000..b2647bf --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Color definitions for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + log_error "Please run as root" + exit 1 +fi + +# Stop and disable service +log_info "Stopping service..." +systemctl stop collision-avoidance +systemctl disable collision-avoidance + +# Remove service file +log_info "Removing service file..." +rm -f /etc/systemd/system/collision-avoidance.service +systemctl daemon-reload + +# Remove files +log_info "Removing files..." +rm -rf /opt/collision_avoidance + +# Remove Python packages +log_info "Removing Python packages..." +pip3 uninstall -y flask werkzeug click itsdangerous Jinja2 MarkupSafe + +# Remove firewall rules +log_info "Removing firewall rules..." +if command -v firewall-cmd &> /dev/null; then + firewall-cmd --permanent --remove-port=8010/tcp + firewall-cmd --permanent --remove-port=8081/tcp + firewall-cmd --reload +else + log_warn "firewalld not detected, please remove firewall rules manually" +fi + +log_info "Uninstallation completed" \ No newline at end of file diff --git a/src/collector/DataCollector.cpp b/src/collector/DataCollector.cpp new file mode 100644 index 0000000..451dd01 --- /dev/null +++ b/src/collector/DataCollector.cpp @@ -0,0 +1,471 @@ +#include "collector/DataCollector.h" +#include "network/HTTPDataSource.h" +#include "utils/Logger.h" +#include +#include +#include +#include + +DataCollector::DataCollector(const AirportBounds& bounds) + : last_position_fetch_(std::chrono::steady_clock::now()), + last_unmanned_fetch_(std::chrono::steady_clock::now()), + last_traffic_light_fetch_(std::chrono::steady_clock::now()), + last_warning_time_(std::chrono::steady_clock::now()), + airportBounds_(bounds) {} + +DataCollector::~DataCollector() { + stop(); +} + +bool DataCollector::initialize(const DataSourceConfig& dataSourceConfig, + const WarnConfig& warnConfig) { + if (!dataSource_) { + dataSourceConfig_ = dataSourceConfig; + dataSource_ = std::make_shared(dataSourceConfig_); + warnConfig_ = warnConfig; + last_position_fetch_ = std::chrono::steady_clock::now(); + last_unmanned_fetch_ = std::chrono::steady_clock::now(); + last_traffic_light_fetch_ = std::chrono::steady_clock::now(); + last_warning_time_ = std::chrono::steady_clock::now(); + } + return true; +} + +void DataCollector::start() { + if (running_) { + return; + } + + if (!dataSource_) { + Logger::error("No data source available"); + return; + } + + running_ = true; + last_position_fetch_ = std::chrono::steady_clock::now(); + last_unmanned_fetch_ = std::chrono::steady_clock::now(); + last_traffic_light_fetch_ = std::chrono::steady_clock::now(); + + // 尝试连接,但即使失败也继续运行 + if (!dataSource_->connect()) { + Logger::error("Failed to connect to data source"); + // 立即检查超时,这样如果一开始就连不上,会马上发出告警 + checkTimeout(); + } + + // 启动三个独立的采集线程 + positionThread_ = std::thread(&DataCollector::positionLoop, this); + unmannedThread_ = std::thread(&DataCollector::unmannedLoop, this); + trafficLightThread_ = std::thread(&DataCollector::trafficLightLoop, this); +} + +void DataCollector::stop() { + if (!running_) { + return; + } + + running_ = false; + + // 等待所有线程结束 + if (positionThread_.joinable()) { + positionThread_.join(); + } + if (unmannedThread_.joinable()) { + unmannedThread_.join(); + } + if (trafficLightThread_.joinable()) { + trafficLightThread_.join(); + } + + if (dataSource_) { + dataSource_->disconnect(); + } +} + +void DataCollector::positionLoop() { + Logger::debug("位置数据采集循环启动,刷新间隔: ", + dataSourceConfig_.position.refresh_interval_ms, "ms"); + auto next_collection_time = std::chrono::steady_clock::now(); + + while (running_) { + bool success = fetchPositionData(); + if (!success) { + Logger::warning("位置数据采集失败"); + checkTimeout(); + } + + // 计算下一次采集时间 + next_collection_time += std::chrono::milliseconds( + dataSourceConfig_.position.refresh_interval_ms); + auto now = std::chrono::steady_clock::now(); + + if (next_collection_time > now) { + auto wait_time = + std::chrono::duration_cast( + next_collection_time - now) + .count(); + if (wait_time > 0) { + std::this_thread::sleep_for( + std::chrono::milliseconds(wait_time)); + } + } else { + // 如果已经超过了下一次采集时间,立即重置 + next_collection_time = std::chrono::steady_clock::now(); + } + } +} + +void DataCollector::unmannedLoop() { + Logger::debug("无人车状态采集循环启动,刷新间隔: ", + dataSourceConfig_.vehicle.refresh_interval_ms, "ms"); + auto next_collection_time = std::chrono::steady_clock::now(); + + while (running_) { + bool success = fetchUnmannedVehicleData(); + if (!success) { + Logger::warning("无人车状态采集失败"); + checkTimeout(); + } + + // 计算下一次采集时间 + next_collection_time += std::chrono::milliseconds( + dataSourceConfig_.vehicle.refresh_interval_ms); + auto now = std::chrono::steady_clock::now(); + + if (next_collection_time > now) { + auto wait_time = + std::chrono::duration_cast( + next_collection_time - now) + .count(); + if (wait_time > 0) { + std::this_thread::sleep_for( + std::chrono::milliseconds(wait_time)); + } + } else { + // 如果已经超过了下一次采集时间,立即重置 + next_collection_time = std::chrono::steady_clock::now(); + } + } +} + +void DataCollector::trafficLightLoop() { + Logger::debug("红绿灯数据采集循环启动,刷新间隔: ", + dataSourceConfig_.traffic_light.refresh_interval_ms, "ms"); + auto next_collection_time = std::chrono::steady_clock::now(); + + while (running_) { + bool success = fetchTrafficLightData(); + if (!success) { + Logger::warning("红绿灯数据采集失败"); + checkTimeout(); + } + + // 计算下一次采集时间 + next_collection_time += std::chrono::milliseconds( + dataSourceConfig_.traffic_light.refresh_interval_ms); + auto now = std::chrono::steady_clock::now(); + + if (next_collection_time > now) { + auto wait_time = + std::chrono::duration_cast( + next_collection_time - now) + .count(); + if (wait_time > 0) { + std::this_thread::sleep_for( + std::chrono::milliseconds(wait_time)); + } + } else { + // 如果已经超过了下一次采集时间,立即重置 + next_collection_time = std::chrono::steady_clock::now(); + } + } +} + +void DataCollector::checkTimeout() { + auto now = std::chrono::steady_clock::now(); + + // 检查位置数据超时 + auto position_elapsed = + std::chrono::duration_cast( + now - last_position_fetch_) + .count(); + if (position_elapsed > dataSourceConfig_.position.timeout_ms) { + sendTimeoutWarning("position", position_elapsed); + } + + // 检查无人车数据超时 + auto unmanned_elapsed = + std::chrono::duration_cast( + now - last_unmanned_fetch_) + .count(); + if (unmanned_elapsed > dataSourceConfig_.vehicle.timeout_ms) { + sendTimeoutWarning("unmanned", unmanned_elapsed); + } + + // 检查红绿灯数据超时 + auto traffic_light_elapsed = + std::chrono::duration_cast( + now - last_traffic_light_fetch_) + .count(); + if (traffic_light_elapsed > dataSourceConfig_.traffic_light.timeout_ms) { + sendTimeoutWarning("traffic_light", traffic_light_elapsed); + } +} + +void DataCollector::resetTimeout(const std::string& source) { + auto now = std::chrono::steady_clock::now(); + + if (source == "position") { + auto elapsed = std::chrono::duration_cast( + now - last_position_fetch_) + .count(); + if (elapsed > dataSourceConfig_.position.timeout_ms) { + Logger::info("Position data source connection restored after ", + elapsed, "ms timeout"); + } + last_position_fetch_ = now; + } else if (source == "unmanned") { + auto elapsed = std::chrono::duration_cast( + now - last_unmanned_fetch_) + .count(); + if (elapsed > dataSourceConfig_.vehicle.timeout_ms) { + Logger::info( + "Unmanned vehicle data source connection restored after ", + elapsed, "ms timeout"); + } + last_unmanned_fetch_ = now; + } else if (source == "traffic_light") { + auto elapsed = std::chrono::duration_cast( + now - last_traffic_light_fetch_) + .count(); + if (elapsed > dataSourceConfig_.traffic_light.timeout_ms) { + Logger::info("Traffic light data source connection restored after ", + elapsed, "ms timeout"); + } + last_traffic_light_fetch_ = now; + } +} + +void DataCollector::sendTimeoutWarning(const std::string& source, + int64_t elapsed_ms) { + auto now = std::chrono::steady_clock::now(); + auto warning_elapsed = + std::chrono::duration_cast( + now - last_warning_time_) + .count(); + + if (warning_elapsed >= warnConfig_.warning_interval_ms) { + if (!system_) { + Logger::debug("System not set, skipping timeout warning"); + return; + } + + network::TimeoutWarningMessage msg; + msg.source = source; + msg.elapsed_ms = elapsed_ms; + + // 根据数据源类型设置超时阈值 + int64_t timeout_ms = 0; + if (source == "position") { + timeout_ms = dataSourceConfig_.position.timeout_ms; + } else if (source == "unmanned") { + timeout_ms = dataSourceConfig_.vehicle.timeout_ms; + } else if (source == "traffic_light") { + timeout_ms = dataSourceConfig_.traffic_light.timeout_ms; + } + + msg.is_read_timeout = (elapsed_ms > timeout_ms); + + system_->broadcastTimeoutWarning(msg); + last_warning_time_ = now; + } +} + +void DataCollector::filterPositionData(std::vector& aircraft, + std::vector& vehicles) { + size_t originalAircraftCount = aircraft.size(); + size_t originalVehicleCount = vehicles.size(); + + // 过滤掉不在机场区域内的航空器数据 + auto aircraftIt = std::remove_if( + aircraft.begin(), aircraft.end(), [this](const Aircraft& a) { + return !airportBounds_.isPointInBounds(a.position); + }); + aircraft.erase(aircraftIt, aircraft.end()); + + // 过滤掉不在机场区域内的车辆数据 + auto vehicleIt = std::remove_if( + vehicles.begin(), vehicles.end(), [this](const Vehicle& v) { + return !airportBounds_.isPointInBounds(v.position); + }); + vehicles.erase(vehicleIt, vehicles.end()); + + // 记录过滤结果 + size_t filteredAircraftCount = originalAircraftCount - aircraft.size(); + size_t filteredVehicleCount = originalVehicleCount - vehicles.size(); + + if (filteredAircraftCount > 0 || filteredVehicleCount > 0) { + Logger::info("数据过滤结果: 过滤掉 ", filteredAircraftCount, + " 架航空器, ", filteredVehicleCount, " 辆车辆"); + } +} + +bool DataCollector::fetchPositionData() { + std::vector newAircraft; + std::vector newVehicles; + + if (!dataSource_) { + Logger::error("No data source available"); + return false; + } + + bool success = dataSource_->fetchPositionAircraftData(newAircraft) && + dataSource_->fetchPositionVehicleData(newVehicles); + + if (success) { + std::lock_guard lock(cacheMutex_); + + // 更新运动信息 + for (auto& a : newAircraft) { + auto it = lastAircraftPositions_.find(a.id); + if (it != lastAircraftPositions_.end()) { + // 复制历史记录 + a.copyHistoryFrom(it->second); + } + // 更新运动信息 + a.updateMotion(a.geo, a.timestamp); + lastAircraftPositions_[a.id] = a; + // 更新时间戳 + lastAircraftTimestamp_ = a.timestamp; + } + + // 更新运动信息 + for (auto& v : newVehicles) { + auto it = lastVehiclePositions_.find(v.id); + if (it != lastVehiclePositions_.end()) { + // 复制历史记录 + v.copyHistoryFrom(it->second); + } + // 更新运动信息 + v.updateMotion(v.geo, v.timestamp); + lastVehiclePositions_[v.id] = v; + // 更新时间戳 + lastVehicleTimestamp_ = v.timestamp; + } + + // 转换坐标系 + for (auto& a : newAircraft) { + // 1. 先转换为世界坐标 + a.position = CoordinateConverter::instance().toLocalXY( + a.geo.latitude, a.geo.longitude); + // 2. 再转换为机场坐标 + a.position = airportBounds_.toAirportCoordinate(a.position); + } + + for (auto& v : newVehicles) { + // 1. 先转换为世界坐标 + v.position = CoordinateConverter::instance().toLocalXY( + v.geo.latitude, v.geo.longitude); + // 2. 再转换为机场坐标 + v.position = airportBounds_.toAirportCoordinate(v.position); + } + + // 过滤数据 + filterPositionData(newAircraft, newVehicles); + + aircraftCache_ = std::move(newAircraft); + vehicleCache_ = std::move(newVehicles); + resetTimeout("position"); + } + + return success; +} + +bool DataCollector::fetchUnmannedVehicleData() { + if (!dataSource_) { + Logger::error("数据源未初始化"); + return false; + } + + bool any_success = false; + bool any_failure = false; + auto& vehicles = ControllableVehicles::getInstance().getVehicles(); + + for (const auto& vehicle : vehicles) { + if (vehicle.type == "UNMANNED") { + std::string status; + if (dataSource_->fetchUnmannedVehicleStatus(vehicle.vehicleNo, + status)) { + any_success = true; + } else { + any_failure = true; + Logger::error("获取无人车状态失败, vehicleNo: " + + vehicle.vehicleNo); + } + } + } + + if (any_success) { + resetTimeout("unmanned"); + // 如果有部分成功,就返回 true + return true; + } + + // 只有全部失败才返回 false + return !any_failure; +} + +bool DataCollector::fetchTrafficLightData() { + std::vector newSignals; + + if (!dataSource_) { + Logger::error("No data source available"); + return false; + } + + bool success = dataSource_->fetchTrafficLightSignals(newSignals); + + if (success) { + std::lock_guard lock(cacheMutex_); + trafficLightCache_ = std::move(newSignals); + // 更新时间戳 + if (!trafficLightCache_.empty()) { + lastTrafficLightTimestamp_ = trafficLightCache_[0].timestamp; + } + resetTimeout("traffic_light"); + } + + return success; +} + +std::vector DataCollector::getTrafficLightSignals() { + std::lock_guard lock(cacheMutex_); + return trafficLightCache_; +} + +bool DataCollector::fetchTrafficLightSignals( + std::vector& signals) { + if (!dataSource_) { + return false; + } + return dataSource_->fetchTrafficLightSignals(signals); +} + +std::vector DataCollector::getAircraftData() { + std::lock_guard lock(cacheMutex_); + return aircraftCache_; +} + +std::vector DataCollector::getVehicleData() { + std::lock_guard lock(cacheMutex_); + return vehicleCache_; +} + +bool DataCollector::fetchUnmannedVehicleStatus(const std::string& vehicle_id, + std::string& status) { + if (!dataSource_) { + Logger::error("No data source available"); + return false; + } + return dataSource_->fetchUnmannedVehicleStatus(vehicle_id, status); +} \ No newline at end of file diff --git a/src/collector/DataCollector.h b/src/collector/DataCollector.h new file mode 100644 index 0000000..70e505b --- /dev/null +++ b/src/collector/DataCollector.h @@ -0,0 +1,117 @@ +#ifndef AIRPORT_COLLECTOR_DATA_COLLECTOR_H +#define AIRPORT_COLLECTOR_DATA_COLLECTOR_H + +#include +#include +#include +#include +#include +#include +#include "DataSource.h" +#include "DataSourceConfig.h" +#include "types/BasicTypes.h" +#include "config/WarnConfig.h" +#include "types/TrafficLightTypes.h" +#include "vehicle/ControllableVehicles.h" +#include "config/AirportBounds.h" + +// 前向声明 +class System; + +class DataCollector { +public: + DataCollector(const AirportBounds& bounds); + ~DataCollector(); + + bool initialize(const DataSourceConfig& dataSourceConfig, const WarnConfig& warnConfig); + void start(); + void stop(); + + // 获取最新数据的接口 + std::tuple, std::vector, std::vector> + getLatestData() const { + std::lock_guard lock(cacheMutex_); + return std::make_tuple(aircraftCache_, vehicleCache_, trafficLightCache_); + } + + // 获取数据的最后更新时间 + std::tuple getLastUpdateTimestamps() const { + std::lock_guard lock(cacheMutex_); + return {lastAircraftTimestamp_, lastVehicleTimestamp_, lastTrafficLightTimestamp_}; + } + + // 获取单个数据的接口 + std::vector getAircraftData(); + std::vector getVehicleData(); + std::vector getTrafficLightSignals(); + bool fetchTrafficLightSignals(std::vector& signals); + bool fetchUnmannedVehicleStatus(const std::string& vehicle_id, std::string& status); + bool fetchPositionData(); // 获取位置数据 + + void setSystem(std::shared_ptr system) { system_ = system; } + void setDataSource(std::shared_ptr source) { dataSource_ = source; } + + // 获取数据的引用访问接口 + const std::vector& getAircraftRef() const { + std::lock_guard lock(cacheMutex_); + return aircraftCache_; + } + + const std::vector& getVehicleRef() const { + std::lock_guard lock(cacheMutex_); + return vehicleCache_; + } + +private: + std::shared_ptr dataSource_; + std::shared_ptr system_; + DataSourceConfig dataSourceConfig_; + WarnConfig warnConfig_; + const AirportBounds& airportBounds_; + + // 三个独立的采集线程 + std::thread positionThread_; // 位置数据采集线程 + std::thread unmannedThread_; // 无人车状态采集线程 + std::thread trafficLightThread_; // 红绿灯数据采集线程 + std::atomic running_{false}; + std::atomic loopCount_{0}; + + // 缓存当前数据 + mutable std::mutex cacheMutex_; + std::vector aircraftCache_; + std::vector vehicleCache_; + std::vector trafficLightCache_; + + // 保存上一次位置信息(用于计算速度) + std::map lastAircraftPositions_; + std::map lastVehiclePositions_; + + // 数据时间戳 + std::atomic lastAircraftTimestamp_{0}; + std::atomic lastVehicleTimestamp_{0}; + std::atomic lastTrafficLightTimestamp_{0}; + + // 超时检测相关 + std::chrono::steady_clock::time_point last_position_fetch_; // 位置数据最后获取时间 + std::chrono::steady_clock::time_point last_unmanned_fetch_; // 无人车状态最后获取时间 + std::chrono::steady_clock::time_point last_traffic_light_fetch_; // 红绿灯数据最后获取时间 + std::chrono::steady_clock::time_point last_warning_time_; + std::chrono::steady_clock::time_point last_log_time_; + + void checkTimeout(); + void resetTimeout(const std::string& source); + void sendTimeoutWarning(const std::string& source, int64_t elapsed_ms); + + // 三个独立的采集循环 + void positionLoop(); // 位置数据采集循环 + void unmannedLoop(); // 无人车状态采集循环 + void trafficLightLoop(); // 红绿灯数据采集循环 + + bool fetchUnmannedVehicleData(); // 获取无人车状态数据 + bool fetchTrafficLightData(); // 获取红绿灯数据 + + // 数据过滤方法 + void filterPositionData(std::vector& aircraft, std::vector& vehicles); +}; + +#endif // AIRPORT_COLLECTOR_DATA_COLLECTOR_H \ No newline at end of file diff --git a/src/collector/DataSource.h b/src/collector/DataSource.h new file mode 100644 index 0000000..a38b720 --- /dev/null +++ b/src/collector/DataSource.h @@ -0,0 +1,38 @@ +#ifndef AIRPORT_COLLECTOR_DATA_SOURCE_H +#define AIRPORT_COLLECTOR_DATA_SOURCE_H + +#include "types/BasicTypes.h" +#include "types/TrafficLightTypes.h" +#include "types/VehicleCommand.h" +#include +#include +#include + +class DataSource { +public: + // 数据源类型 + enum class DataSourceType { + POSITION, // 位置数据接口 + UNMANNED, // 无人车接口 + TRAFFIC_LIGHT // 红绿灯接口 + }; + + virtual ~DataSource() = default; + + virtual bool connect() = 0; + virtual void disconnect() = 0; + virtual bool isAvailable() const = 0; + + // 位置数据接口 + virtual bool fetchPositionAircraftData(std::vector& aircraft) = 0; + virtual bool fetchPositionVehicleData(std::vector& vehicles) = 0; + + // 无人车接口 + virtual bool sendUnmannedVehicleCommand(const std::string& vehicle_id, const VehicleCommand& command) = 0; + virtual bool fetchUnmannedVehicleStatus(const std::string& vehicle_id, std::string& status) = 0; + + // 红绿灯接口 + virtual bool fetchTrafficLightSignals(std::vector& signals) = 0; +}; + +#endif // AIRPORT_COLLECTOR_DATA_SOURCE_H \ No newline at end of file diff --git a/src/collector/DataSourceConfig.cpp b/src/collector/DataSourceConfig.cpp new file mode 100644 index 0000000..aa6fd74 --- /dev/null +++ b/src/collector/DataSourceConfig.cpp @@ -0,0 +1,72 @@ +#include "DataSourceConfig.h" +#include "config/SystemConfig.h" +#include "utils/Logger.h" + +DataSourceConfig DataSourceConfig::fromSystemConfig(const SystemConfig& config) { + DataSourceConfig dataSourceConfig; + + // 转换位置数据源配置 + dataSourceConfig.position.host = config.data_source.position.host; + dataSourceConfig.position.port = config.data_source.position.port; + dataSourceConfig.position.aircraft_path = + config.data_source.position.aircraft_path; + dataSourceConfig.position.vehicle_path = + config.data_source.position.vehicle_path; + dataSourceConfig.position.refresh_interval_ms = + config.data_source.position.refresh_interval_ms; + dataSourceConfig.position.timeout_ms = + config.data_source.position.timeout_ms; + dataSourceConfig.position.read_timeout_ms = + config.data_source.position.read_timeout_ms; + dataSourceConfig.position.auth.username = + config.data_source.position.auth.username; + dataSourceConfig.position.auth.password = + config.data_source.position.auth.password; + dataSourceConfig.position.auth.auth_path = + config.data_source.position.auth.auth_path; + dataSourceConfig.position.auth.auth_required = + config.data_source.position.auth.auth_required; + + // 转换无人车数据源配置 + dataSourceConfig.vehicle.host = config.data_source.vehicle.host; + dataSourceConfig.vehicle.port = config.data_source.vehicle.port; + dataSourceConfig.vehicle.status_path = + config.data_source.vehicle.status_path; + dataSourceConfig.vehicle.command_path = + config.data_source.vehicle.command_path; + dataSourceConfig.vehicle.refresh_interval_ms = + config.data_source.vehicle.refresh_interval_ms; + dataSourceConfig.vehicle.timeout_ms = config.data_source.vehicle.timeout_ms; + dataSourceConfig.vehicle.read_timeout_ms = + config.data_source.vehicle.read_timeout_ms; + dataSourceConfig.vehicle.auth.username = + config.data_source.vehicle.auth.username; + dataSourceConfig.vehicle.auth.password = + config.data_source.vehicle.auth.password; + dataSourceConfig.vehicle.auth.auth_path = + config.data_source.vehicle.auth.auth_path; + dataSourceConfig.vehicle.auth.auth_required = + config.data_source.vehicle.auth.auth_required; + + // 转换红绿灯数据源配置 + dataSourceConfig.traffic_light.host = config.data_source.traffic_light.host; + dataSourceConfig.traffic_light.port = config.data_source.traffic_light.port; + dataSourceConfig.traffic_light.signal_path = + config.data_source.traffic_light.signal_path; + dataSourceConfig.traffic_light.refresh_interval_ms = + config.data_source.traffic_light.refresh_interval_ms; + dataSourceConfig.traffic_light.timeout_ms = + config.data_source.traffic_light.timeout_ms; + dataSourceConfig.traffic_light.read_timeout_ms = + config.data_source.traffic_light.read_timeout_ms; + dataSourceConfig.traffic_light.auth.username = + config.data_source.traffic_light.auth.username; + dataSourceConfig.traffic_light.auth.password = + config.data_source.traffic_light.auth.password; + dataSourceConfig.traffic_light.auth.auth_path = + config.data_source.traffic_light.auth.auth_path; + dataSourceConfig.traffic_light.auth.auth_required = + config.data_source.traffic_light.auth.auth_required; + + return dataSourceConfig; +} \ No newline at end of file diff --git a/src/collector/DataSourceConfig.h b/src/collector/DataSourceConfig.h new file mode 100644 index 0000000..650fb34 --- /dev/null +++ b/src/collector/DataSourceConfig.h @@ -0,0 +1,171 @@ +#ifndef AIRPORT_COLLECTOR_DATA_SOURCE_CONFIG_H +#define AIRPORT_COLLECTOR_DATA_SOURCE_CONFIG_H + +#include +#include +#include + +using json = nlohmann::json; + +// 前向声明 +class SystemConfig; + +// 认证配置 +struct AuthConfig { + std::string username; // 认证用户名 + std::string password; // 认证密码 + std::string auth_path; // 认证接口路径 + bool auth_required = false; // 是否需要认证 +}; + +// AuthConfig 的 JSON 序列化/反序列化函数 +inline void to_json(json& j, const AuthConfig& a) { + j = json{ + {"username", a.username}, + {"password", a.password}, + {"auth_path", a.auth_path}, + {"auth_required", a.auth_required} + }; +} + +inline void from_json(const json& j, AuthConfig& a) { + j.at("username").get_to(a.username); + j.at("password").get_to(a.password); + j.at("auth_path").get_to(a.auth_path); + j.at("auth_required").get_to(a.auth_required); +} + +// 位置数据源配置 +struct PositionDataConfig { + std::string host; // 服务器地址 + int port; // 服务器端口 + std::string aircraft_path; // 航空器位置接口 + std::string vehicle_path; // 车辆位置接口 + int refresh_interval_ms; // 位置数据刷新间隔 + int timeout_ms; // 连接超时时间 + int read_timeout_ms; // 读取超时时间 + AuthConfig auth; // 认证配置 +}; + +// PositionDataConfig 的 JSON 序列化/反序列化函数 +inline void to_json(json& j, const PositionDataConfig& p) { + j = json{ + {"host", p.host}, + {"port", p.port}, + {"aircraft_path", p.aircraft_path}, + {"vehicle_path", p.vehicle_path}, + {"refresh_interval_ms", p.refresh_interval_ms}, + {"timeout_ms", p.timeout_ms}, + {"read_timeout_ms", p.read_timeout_ms}, + {"auth", p.auth} + }; +} + +inline void from_json(const json& j, PositionDataConfig& p) { + j.at("host").get_to(p.host); + j.at("port").get_to(p.port); + j.at("aircraft_path").get_to(p.aircraft_path); + j.at("vehicle_path").get_to(p.vehicle_path); + j.at("refresh_interval_ms").get_to(p.refresh_interval_ms); + j.at("timeout_ms").get_to(p.timeout_ms); + j.at("read_timeout_ms").get_to(p.read_timeout_ms); + j.at("auth").get_to(p.auth); +} + +// 无人车数据源配置 +struct UnmannedVehicleConfig { + std::string host; // 服务器地址 + int port; // 服务器端口 + std::string status_path; // 无人车状态接口 + std::string command_path; // 无人车指令接口 + int refresh_interval_ms; // 状态数据刷新间隔 + int timeout_ms; // 连接超时时间 + int read_timeout_ms; // 读取超时时间 + AuthConfig auth; // 认证配置 +}; + +// UnmannedVehicleConfig 的 JSON 序列化/反序列化函数 +inline void to_json(json& j, const UnmannedVehicleConfig& u) { + j = json{ + {"host", u.host}, + {"port", u.port}, + {"status_path", u.status_path}, + {"command_path", u.command_path}, + {"refresh_interval_ms", u.refresh_interval_ms}, + {"timeout_ms", u.timeout_ms}, + {"read_timeout_ms", u.read_timeout_ms}, + {"auth", u.auth} + }; +} + +inline void from_json(const json& j, UnmannedVehicleConfig& u) { + j.at("host").get_to(u.host); + j.at("port").get_to(u.port); + j.at("status_path").get_to(u.status_path); + j.at("command_path").get_to(u.command_path); + j.at("refresh_interval_ms").get_to(u.refresh_interval_ms); + j.at("timeout_ms").get_to(u.timeout_ms); + j.at("read_timeout_ms").get_to(u.read_timeout_ms); + j.at("auth").get_to(u.auth); +} + +// 红绿灯数据源配置 +struct TrafficLightConfig { + std::string host; // 服务器地址 + int port; // 服务器端口 + std::string signal_path; // 红绿灯信号接口 + int refresh_interval_ms; // 信号数据刷新间隔 + int timeout_ms; // 连接超时时间 + int read_timeout_ms; // 读取超时时间 + AuthConfig auth; // 认证配置 +}; + +// TrafficLightConfig 的 JSON 序列化/反序列化函数 +inline void to_json(json& j, const TrafficLightConfig& t) { + j = json{ + {"host", t.host}, + {"port", t.port}, + {"signal_path", t.signal_path}, + {"refresh_interval_ms", t.refresh_interval_ms}, + {"timeout_ms", t.timeout_ms}, + {"read_timeout_ms", t.read_timeout_ms}, + {"auth", t.auth} + }; +} + +inline void from_json(const json& j, TrafficLightConfig& t) { + j.at("host").get_to(t.host); + j.at("port").get_to(t.port); + j.at("signal_path").get_to(t.signal_path); + j.at("refresh_interval_ms").get_to(t.refresh_interval_ms); + j.at("timeout_ms").get_to(t.timeout_ms); + j.at("read_timeout_ms").get_to(t.read_timeout_ms); + j.at("auth").get_to(t.auth); +} + +// 数据源配置 +struct DataSourceConfig { + PositionDataConfig position; // 位置数据源配置 + UnmannedVehicleConfig vehicle; // 无人车数据源配置 + TrafficLightConfig traffic_light; // 红绿灯数据源配置 + + // 从系统配置创建数据源配置的静态工厂方法 + static DataSourceConfig fromSystemConfig(const SystemConfig& system_config); +}; + +// DataSourceConfig 的 JSON 序列化/反序列化函数 +inline void to_json(json& j, const DataSourceConfig& d) { + j = json{ + {"position", d.position}, + {"unmanned_vehicle", d.vehicle}, + {"traffic_light", d.traffic_light} + }; +} + +inline void from_json(const json& j, DataSourceConfig& d) { + j.at("position").get_to(d.position); + j.at("unmanned_vehicle").get_to(d.vehicle); + j.at("traffic_light").get_to(d.traffic_light); +} + +#endif diff --git a/src/config/AirportBounds.cpp b/src/config/AirportBounds.cpp new file mode 100644 index 0000000..2e45a1e --- /dev/null +++ b/src/config/AirportBounds.cpp @@ -0,0 +1,179 @@ +#include "AirportBounds.h" +#include "utils/Logger.h" +#include +#include +#include +#include + +using json = nlohmann::json; + +AirportBounds::AirportBounds(const std::string& configFile) { + // 如果配置文件路径为空,不加载配置 + if (!configFile.empty()) { + loadConfig(configFile); + } +} + +void AirportBounds::loadConfig(const std::string& configFile) { + // 检查文件是否存在 + if (!std::filesystem::exists(configFile)) { + Logger::error("配置文件不存在: ", configFile); + Logger::error("当前工作目录: ", + std::filesystem::current_path().string()); + throw std::runtime_error("配置文件不存在: " + configFile); + } + + std::ifstream file(configFile); + if (!file.is_open()) { + throw std::runtime_error("无法打开配置文件: " + configFile); + } + + try { + json j; + file >> j; + + // 检查必要的字段是否存在 + if (!j.contains("airport") || !j["airport"].contains("bounds")) { + throw std::runtime_error("配置文件缺少 airport.bounds 字段"); + } + + // 加载机场旋转角度(度)和参考点 + auto& airport = j["airport"]; + if (airport.contains("rotation_angle")) { + // 将角度转换为弧度 + rotationAngle_ = + airport["rotation_angle"].get() * M_PI / 180.0; + } + + if (airport.contains("reference_point")) { + referencePoint_.x = airport["reference_point"]["x"].get(); + referencePoint_.y = airport["reference_point"]["y"].get(); + } + + // 加载机场边界 + auto& airportBounds = j["airport"]["bounds"]; + airportBounds_ = { airportBounds["x"].get(), + airportBounds["y"].get(), + airportBounds["width"].get(), + airportBounds["height"].get() }; + + // 检查 areas 字段 + if (!j.contains("areas")) { + throw std::runtime_error("配置文件缺少 areas 字段"); + } + + // 加载各区域配置 + for (const auto& [key, value] : j["areas"].items()) { + AreaType type; + if (key == "runway") + type = AreaType::RUNWAY; + else if (key == "taxiway") + type = AreaType::TAXIWAY; + else if (key == "gate") + type = AreaType::GATE; + else if (key == "service") + type = AreaType::SERVICE; + else if (key == "test_zone") + type = AreaType::TEST_ZONE; + else + continue; + + // 检查必要的字段 + if (!value.contains("bounds") || !value.contains("config")) { + throw std::runtime_error("区域 " + key + " 缺少必要的字段"); + } + + // 加载区域边界 + auto& bounds = value["bounds"]; + areaBounds_[type] = { + bounds["x"].get(), bounds["y"].get(), + bounds["width"].get(), bounds["height"].get() }; + + // 加载区域配置 + auto& config = value["config"]; + auto& collision_radius = config["collision_radius"]; + auto& warning_zone_radius = config["warning_zone_radius"]; + auto& alert_zone_radius = config["alert_zone_radius"]; + + RadiusConfig collision_config = { + collision_radius["aircraft"].get(), + collision_radius["special"].get(), + collision_radius["unmanned"].get() }; + + RadiusConfig warning_config = { + warning_zone_radius["aircraft"].get(), + warning_zone_radius["special"].get(), + warning_zone_radius["unmanned"].get() }; + + RadiusConfig alert_config = { + alert_zone_radius["aircraft"].get(), + alert_zone_radius["special"].get(), + alert_zone_radius["unmanned"].get() }; + + areaConfigs_[type] = { collision_config, + config["height_threshold"].get(), + warning_config, alert_config }; + } + } catch (const json::exception& e) { + Logger::error("JSON解析错误: ", e.what()); + throw; + } catch (const std::exception& e) { + Logger::error("加载配置文件失败: ", e.what()); + throw; + } +} + +Vector2D AirportBounds::toAirportCoordinate(const Vector2D& point) const { + // 1. 计算相对于参考点的偏移 + double dx = point.x - referencePoint_.x; + double dy = point.y - referencePoint_.y; + + // 2. 应用旋转变换 + double cos_angle = std::cos(rotationAngle_); + double sin_angle = std::sin(rotationAngle_); + + // 3. 返回机场坐标系中的坐标(逆时针旋转) + Vector2D result{ dx * cos_angle - dy * sin_angle, + dx * sin_angle + dy * cos_angle }; + + return result; +} + +AreaType AirportBounds::getAreaType(const Vector2D& position) const { + + // 按优先级检查位置所在区域 + for (const auto& [type, bounds] : areaBounds_) { + if (bounds.contains(position)) { + return type; + } + } + + // 默认返回测试区 + return AreaType::TEST_ZONE; +} + +const AreaConfig& AirportBounds::getAreaConfig(AreaType type) const { + auto it = areaConfigs_.find(type); + if (it == areaConfigs_.end()) { + throw std::runtime_error("Invalid area type"); + } + return it->second; +} + +bool AirportBounds::isPointInBounds(const Vector2D& position) const { + // 在机场坐标系中判断是否在边界内 + bool result = airportBounds_.contains(position); + return result; +} + +bool AirportBounds::isPointInArea(const Vector2D& position, + AreaType areaType) const { + // 获取区域定义 + auto it = areaBounds_.find(areaType); + if (it == areaBounds_.end()) { + return false; + } + + // 在机场坐标系中判断是否在区域内 + return it->second.contains(position); +} \ No newline at end of file diff --git a/src/config/AirportBounds.h b/src/config/AirportBounds.h new file mode 100644 index 0000000..55d212a --- /dev/null +++ b/src/config/AirportBounds.h @@ -0,0 +1,58 @@ +#ifndef AIRPORT_BOUNDS_H +#define AIRPORT_BOUNDS_H +#include +#include +#include +#include "types/BasicTypes.h" +#include "spatial/QuadTree.h" +#include "AreaConfig.h" +#include + +// 机场区域定义 +class AirportBounds { +public: + explicit AirportBounds(const std::string& configFile = ""); + virtual ~AirportBounds() = default; + + // 获取点所在的区域类型 + virtual AreaType getAreaType(const Vector2D& position) const; + + // 获取区域配置 + virtual const AreaConfig& getAreaConfig(AreaType type) const; + + // 获取整个机场边界 + virtual const Bounds& getAirportBounds() const { return airportBounds_; } + + // 获取特定区域的边界 + virtual const Bounds& getAreaBounds(AreaType type) const { + auto it = areaBounds_.find(type); + if (it == areaBounds_.end()) { + throw std::runtime_error("Invalid area type"); + } + return it->second; + } + + // 判断点是否在机场边界内 + virtual bool isPointInBounds(const Vector2D& worldPoint) const; + + // 判断点是否在指定区域内(跑道、滑行道等) + virtual bool isPointInArea(const Vector2D& worldPoint, AreaType areaType) const; + + // 将点从世界坐标系转换到机场坐标系 + // 输入:世界坐标系中的点 (x, y) + // 输出:机场坐标系中的点,原点为机场参考点,方向为机场旋转后的方向 + Vector2D toAirportCoordinate(const Vector2D& point) const; + +protected: + Bounds airportBounds_; // 整个机场边界 + std::unordered_map areaBounds_; // 各区域边界 + std::unordered_map areaConfigs_; // 各区域配置 + + // 从配置文件加载数据 + virtual void loadConfig(const std::string& configFile); + + double rotationAngle_ = 0.0; // 机场与正北方向的夹角(弧度) + Vector2D referencePoint_; // 机场参考点(旋转中心) +}; + +#endif // AIRPORT_BOUNDS_H \ No newline at end of file diff --git a/src/config/AreaConfig.h b/src/config/AreaConfig.h new file mode 100644 index 0000000..373fae3 --- /dev/null +++ b/src/config/AreaConfig.h @@ -0,0 +1,53 @@ +#ifndef AIRPORT_CONFIG_AREA_CONFIG_H +#define AIRPORT_CONFIG_AREA_CONFIG_H + +#include "types/BasicTypes.h" + +// 区域类型 +enum class AreaType { + RUNWAY, // 跑道 + TAXIWAY, // 滑行道 + GATE, // 停机位 + SERVICE, // 服务区 + TEST_ZONE // 测试区 +}; + +// 半径配置 +struct RadiusConfig { + double aircraft; // 航空器半径 + double special; // 特勤车半径 + double unmanned; // 无人车半径 + + double getRadius(MovingObjectType type) const { + switch (type) { + case MovingObjectType::AIRCRAFT: return aircraft; + case MovingObjectType::SPECIAL: return special; + case MovingObjectType::UNMANNED: return unmanned; + } + return special; // 默认返回特勤车半径 + } +}; + +// 区域配置 +struct AreaConfig { + RadiusConfig collision_radius; // 碰撞半径 + double height_threshold; // 高度阈值 + RadiusConfig warning_zone_radius; // 预警区域半径 + RadiusConfig alert_zone_radius; // 告警区域半径 + + // 获取碰撞阈值 + struct CollisionThresholds { + double warning; // 预警距离 + double critical; // 告警距离 + }; + + // 根据物体类型获取对应的阈值 + CollisionThresholds getThresholds(MovingObjectType type1, MovingObjectType type2) const { + return { + warning_zone_radius.getRadius(type1) + warning_zone_radius.getRadius(type2), + alert_zone_radius.getRadius(type1) + alert_zone_radius.getRadius(type2) + }; + } +}; + +#endif // AIRPORT_CONFIG_AREA_CONFIG_H \ No newline at end of file diff --git a/src/config/IntersectionConfig.cpp b/src/config/IntersectionConfig.cpp new file mode 100644 index 0000000..f869465 --- /dev/null +++ b/src/config/IntersectionConfig.cpp @@ -0,0 +1,78 @@ +#include "config/IntersectionConfig.h" +#include "utils/Logger.h" +#include +#include +#include +#include + +IntersectionConfig IntersectionConfig::load(const std::string& configFile) { + IntersectionConfig config; + + std::ifstream file(configFile); + if (!file.is_open()) { + throw std::runtime_error("Failed to open intersection config file: " + configFile); + } + + try { + nlohmann::json jsonConfig; + file >> jsonConfig; + + for (const auto& item : jsonConfig["intersections"]) { + Intersection info; + info.id = item["id"].get(); + info.name = item["name"].get(); + info.trafficLightId = item["trafficLightId"].get(); + + // 加载位置信息,检查是否为 null + if (item["position"]["longitude"].is_null() || + item["position"]["latitude"].is_null() || + item["position"]["altitude"].is_null()) { + throw std::runtime_error("Position values cannot be null for intersection: " + info.id); + } + info.position.longitude = item["position"]["longitude"].get(); + info.position.latitude = item["position"]["latitude"].get(); + info.position.altitude = item["position"]["altitude"].get(); + + // 加载路口宽度和安全区配置,检查是否为 null + if (item["width"].is_null() || + item["safetyZone"]["aircraftRadius"].is_null() || + item["safetyZone"]["vehicleRadius"].is_null()) { + throw std::runtime_error("Width or safety zone values cannot be null for intersection: " + info.id); + } + info.width = item["width"].get(); + info.safetyZone.aircraftRadius = item["safetyZone"]["aircraftRadius"].get(); + info.safetyZone.vehicleRadius = item["safetyZone"]["vehicleRadius"].get(); + + config.intersections_.push_back(info); + + Logger::debug("Loaded intersection: id=", info.id, + ", name=", info.name, + ", trafficLightId=", info.trafficLightId, + ", width=", info.width, + ", aircraftRadius=", info.safetyZone.aircraftRadius, + ", vehicleRadius=", info.safetyZone.vehicleRadius); + } + + Logger::info("Loaded ", config.intersections_.size(), " intersections"); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to parse intersection config: " + std::string(e.what())); + } + + return config; +} + +const Intersection* IntersectionConfig::findByTrafficLightId(const std::string& trafficLightId) const { + auto iter = std::find_if(intersections_.begin(), intersections_.end(), + [&](const Intersection& info) { + return info.trafficLightId == trafficLightId; + }); + return iter != intersections_.end() ? &(*iter) : nullptr; +} + +const Intersection* IntersectionConfig::findById(const std::string& intersectionId) const { + auto iter = std::find_if(intersections_.begin(), intersections_.end(), + [&](const Intersection& info) { + return info.id == intersectionId; + }); + return iter != intersections_.end() ? &(*iter) : nullptr; +} \ No newline at end of file diff --git a/src/config/IntersectionConfig.h b/src/config/IntersectionConfig.h new file mode 100644 index 0000000..e7295aa --- /dev/null +++ b/src/config/IntersectionConfig.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include "types/BasicTypes.h" + +struct IntersectionPosition { + double longitude; + double latitude; + double altitude; +}; + +// 路口安全区配置 +struct SafetyZoneConfig { + double aircraftRadius; // 航空器安全区半径 + double vehicleRadius; // 特勤车安全区半径 +}; + +struct Intersection { + std::string id; + std::string name; + std::string trafficLightId; + IntersectionPosition position; + double width; // 路口宽度,单位:米 + SafetyZoneConfig safetyZone; // 安全区配置 +}; + +class IntersectionConfig { +public: + static IntersectionConfig load(const std::string& configFile); + + // 根据红绿灯ID查找路口 + const Intersection* findByTrafficLightId(const std::string& trafficLightId) const; + + // 根据路口ID查找路口 + const Intersection* findById(const std::string& intersectionId) const; + + // 获取所有路口配置 + const std::vector& getIntersections() const { return intersections_; } + +private: + std::vector intersections_; +}; \ No newline at end of file diff --git a/src/config/SystemConfig.cpp b/src/config/SystemConfig.cpp new file mode 100644 index 0000000..6f2a9aa --- /dev/null +++ b/src/config/SystemConfig.cpp @@ -0,0 +1,21 @@ +#include "SystemConfig.h" +#include "utils/Logger.h" +#include + +void SystemConfig::load(const std::string& filename) { + try { + std::ifstream file(filename); + if (!file.is_open()) { + throw std::runtime_error("Failed to open config file: " + filename); + } + + nlohmann::json j; + file >> j; + nlohmann::from_json(j, *this); + + Logger::info("Loaded system configuration from ", filename); + } catch (const std::exception& e) { + throw std::runtime_error("Failed to load config: " + + std::string(e.what())); + } +} \ No newline at end of file diff --git a/src/config/SystemConfig.h b/src/config/SystemConfig.h new file mode 100644 index 0000000..d673c48 --- /dev/null +++ b/src/config/SystemConfig.h @@ -0,0 +1,293 @@ +#pragma once + +#include +#include +#include +#include "collector/DataSourceConfig.h" + +using json = nlohmann::json; + +class System; // 前向声明 + +// 基础结构体 +struct CoordinatePoint { + std::string point; + double latitude; + double longitude; +}; + +// JSON 序列化/反序列化函数 +inline void to_json(json& j, const CoordinatePoint& p) { + j = json{ + {"point", p.point}, + {"latitude", p.latitude}, + {"longitude", p.longitude} + }; +} + +inline void from_json(const json& j, CoordinatePoint& p) { + j.at("point").get_to(p.point); + j.at("latitude").get_to(p.latitude); + j.at("longitude").get_to(p.longitude); +} + +// SystemConfig 类定义 +class SystemConfig { +public: + struct Airport { + struct ReferencePoint { + double latitude; + double longitude; + } reference_point; + + std::string name; + std::string iata; + std::string icao; + std::vector coordinate_points; + } airport; + + DataSourceConfig data_source; // 使用 DataSourceConfig 替换原有的 DataSource 结构 + + struct WebSocket { + struct PositionUpdate { + int aircraft_interval_ms; + int vehicle_interval_ms; + int traffic_light_interval_ms; + } position_update; + + uint16_t port; + int max_connections; + int ping_interval_ms; + } websocket; + + struct CollisionDetection { + struct Prediction { + double time_window; + double vehicle_size; + double aircraft_size; + double min_unmanned_speed; + } prediction; + + int update_interval_ms; + } collision_detection; + + struct Logging { + std::string level; + std::string file; + int max_size_mb; + int max_files; + bool console_output; + } logging; + + struct Debug { + bool enable_mock_data; + bool save_raw_data; + bool profile_performance; + } debug; + + struct Warning { + int warning_interval_ms; + int log_interval_ms; + } warning; + + // 新增: 红绿灯 HTTP 服务器配置 + struct TrafficLightServerConfig { + uint16_t port = 8082; // Default port if not specified in config + int max_connections = 100; + // Can add other relevant configs like timeout etc. + } traffic_light_server; + + // 新增: 模拟的移动红绿灯目标路口 ID + std::string simulated_mobile_light_target_intersection_id = "T2路口"; // Default value + + static SystemConfig& instance() { + static SystemConfig instance; + return instance; + } + + void load(const std::string& filename); + + SystemConfig(const SystemConfig&) = delete; + SystemConfig& operator=(const SystemConfig&) = delete; + + friend class System; + +private: + SystemConfig() = default; +}; + +// 为每个嵌套结构体定义 JSON 序列化/反序列化函数 +inline void to_json(json& j, const SystemConfig::Airport::ReferencePoint& p) { + j = json{ + {"latitude", p.latitude}, + {"longitude", p.longitude} + }; +} + +inline void from_json(const json& j, SystemConfig::Airport::ReferencePoint& p) { + j.at("latitude").get_to(p.latitude); + j.at("longitude").get_to(p.longitude); +} + +inline void to_json(json& j, const SystemConfig::Airport& p) { + j = json{ + {"name", p.name}, + {"iata", p.iata}, + {"icao", p.icao}, + {"reference_point", p.reference_point}, + {"coordinate_points", p.coordinate_points} + }; +} + +inline void from_json(const json& j, SystemConfig::Airport& p) { + j.at("name").get_to(p.name); + j.at("iata").get_to(p.iata); + j.at("icao").get_to(p.icao); + j.at("reference_point").get_to(p.reference_point); + j.at("coordinate_points").get_to(p.coordinate_points); +} + +inline void to_json(json& j, const SystemConfig::WebSocket::PositionUpdate& p) { + j = json{ + {"aircraft_interval_ms", p.aircraft_interval_ms}, + {"vehicle_interval_ms", p.vehicle_interval_ms}, + {"traffic_light_interval_ms", p.traffic_light_interval_ms} + }; +} + +inline void from_json(const json& j, SystemConfig::WebSocket::PositionUpdate& p) { + j.at("aircraft_interval_ms").get_to(p.aircraft_interval_ms); + j.at("vehicle_interval_ms").get_to(p.vehicle_interval_ms); + j.at("traffic_light_interval_ms").get_to(p.traffic_light_interval_ms); +} + +inline void to_json(json& j, const SystemConfig::WebSocket& p) { + j = json{ + {"port", p.port}, + {"max_connections", p.max_connections}, + {"ping_interval_ms", p.ping_interval_ms}, + {"position_update", p.position_update} + }; +} + +inline void from_json(const json& j, SystemConfig::WebSocket& p) { + j.at("port").get_to(p.port); + j.at("max_connections").get_to(p.max_connections); + j.at("ping_interval_ms").get_to(p.ping_interval_ms); + j.at("position_update").get_to(p.position_update); +} + +inline void to_json(json& j, const SystemConfig::CollisionDetection::Prediction& p) { + j = json{ + {"time_window", p.time_window}, + {"vehicle_size", p.vehicle_size}, + {"aircraft_size", p.aircraft_size}, + {"min_unmanned_speed", p.min_unmanned_speed} + }; +} + +inline void from_json(const json& j, SystemConfig::CollisionDetection::Prediction& p) { + j.at("time_window").get_to(p.time_window); + j.at("vehicle_size").get_to(p.vehicle_size); + j.at("aircraft_size").get_to(p.aircraft_size); + j.at("min_unmanned_speed").get_to(p.min_unmanned_speed); +} + +inline void to_json(json& j, const SystemConfig::CollisionDetection& p) { + j = json{ + {"update_interval_ms", p.update_interval_ms}, + {"prediction", p.prediction} + }; +} + +inline void from_json(const json& j, SystemConfig::CollisionDetection& p) { + j.at("update_interval_ms").get_to(p.update_interval_ms); + j.at("prediction").get_to(p.prediction); +} + +inline void to_json(json& j, const SystemConfig::Logging& p) { + j = json{ + {"level", p.level}, + {"file", p.file}, + {"max_size_mb", p.max_size_mb}, + {"max_files", p.max_files}, + {"console_output", p.console_output} + }; +} + +inline void from_json(const json& j, SystemConfig::Logging& p) { + j.at("level").get_to(p.level); + j.at("file").get_to(p.file); + j.at("max_size_mb").get_to(p.max_size_mb); + j.at("max_files").get_to(p.max_files); + j.at("console_output").get_to(p.console_output); +} + +inline void to_json(json& j, const SystemConfig::Debug& p) { + j = json{ + {"enable_mock_data", p.enable_mock_data}, + {"save_raw_data", p.save_raw_data}, + {"profile_performance", p.profile_performance} + }; +} + +inline void from_json(const json& j, SystemConfig::Debug& p) { + j.at("enable_mock_data").get_to(p.enable_mock_data); + j.at("save_raw_data").get_to(p.save_raw_data); + j.at("profile_performance").get_to(p.profile_performance); +} + +inline void to_json(json& j, const SystemConfig::Warning& p) { + j = json{ + {"warning_interval_ms", p.warning_interval_ms}, + {"log_interval_ms", p.log_interval_ms} + }; +} + +inline void from_json(const json& j, SystemConfig::Warning& p) { + j.at("warning_interval_ms").get_to(p.warning_interval_ms); + j.at("log_interval_ms").get_to(p.log_interval_ms); +} + +// 新增: TrafficLightServerConfig 的 JSON 序列化/反序列化函数 +inline void to_json(json& j, const SystemConfig::TrafficLightServerConfig& p) { + j = json{ + {"port", p.port}, + {"max_connections", p.max_connections} + // Serialize other fields if added + }; +} + +inline void from_json(const json& j, SystemConfig::TrafficLightServerConfig& p) { + // Use .value() to provide default values if keys are missing + p.port = j.value("port", static_cast(8082)); + p.max_connections = j.value("max_connections", 100); + // Deserialize other fields if added +} + +inline void to_json(json& j, const SystemConfig& config) { + j = json{ + {"airport", config.airport}, + {"data_source", config.data_source}, + {"websocket", config.websocket}, + {"collision_detection", config.collision_detection}, + {"logging", config.logging}, + {"debug", config.debug}, + {"warning", config.warning}, + {"traffic_light_server", config.traffic_light_server}, + {"simulated_mobile_light_target_intersection_id", config.simulated_mobile_light_target_intersection_id} + }; +} + +inline void from_json(const json& j, SystemConfig& config) { + j.at("airport").get_to(config.airport); + j.at("data_source").get_to(config.data_source); + j.at("websocket").get_to(config.websocket); + j.at("collision_detection").get_to(config.collision_detection); + j.at("logging").get_to(config.logging); + j.at("debug").get_to(config.debug); + j.at("warning").get_to(config.warning); + config.traffic_light_server = j.value("traffic_light_server", SystemConfig::TrafficLightServerConfig{}); + config.simulated_mobile_light_target_intersection_id = j.value("simulated_mobile_light_target_intersection_id", std::string("T2路口")); +} + diff --git a/src/config/WarnConfig.h b/src/config/WarnConfig.h new file mode 100644 index 0000000..9c87c37 --- /dev/null +++ b/src/config/WarnConfig.h @@ -0,0 +1,11 @@ +#ifndef AIRPORT_NETWORK_WARN_CONFIG_H +#define AIRPORT_NETWORK_WARN_CONFIG_H + +#include + +struct WarnConfig { + int warning_interval_ms; + int log_interval_ms; +}; + +#endif // AIRPORT_NETWORK_WARN_CONFIG_H \ No newline at end of file diff --git a/src/core/System.cpp b/src/core/System.cpp new file mode 100644 index 0000000..89848a1 --- /dev/null +++ b/src/core/System.cpp @@ -0,0 +1,856 @@ +#include +#include +#include +#include +#include "System.h" +#include "utils/Logger.h" +#include "collector/DataCollector.h" +#include "spatial/CoordinateConverter.h" +#include "network/TrafficLightHttpServer.h" +#include "config/SystemConfig.h" +#include "types/TrafficLightTypes.h" +#include "network/WebSocketServer.h" +#include + +System* System::instance_ = nullptr; + +System::System() + : controllableVehicles_(ControllableVehicles::getInstance()) { + instance_ = this; + std::signal(SIGINT, signalHandler); + std::signal(SIGTERM, signalHandler); + + // 使用单例模式获取配置实例 + auto& config = SystemConfig::instance(); +} + +System::~System() { + stop(); + instance_ = nullptr; +} + +void System::signalHandler(int signal) { + Logger::info("Received signal: ", signal); + if (instance_) { + instance_->stop(); + } + std::exit(0); +} + +bool System::initialize() { + try { + // 配置已在 main 中加载,这里直接使用 + const auto& system_config = SystemConfig::instance(); + + // 初始化全局坐标转换器 + CoordinateConverter::instance().setReferencePoint( + system_config.airport.reference_point.latitude, + system_config.airport.reference_point.longitude); + + // 加载路口配置 + intersection_config_ = IntersectionConfig::load("config/intersections.json"); + Logger::info("Loaded {} intersections", intersection_config_.getIntersections().size()); + + // 初始化 WebSocket 服务器 + ws_server_ = std::make_unique(system_config.websocket.port); + ws_thread_ = std::thread([this]() { ws_server_->start(); }); + Logger::info("WebSocket server initialized on port ", system_config.websocket.port); + + // 新增: 初始化并启动红绿灯 HTTP 服务器 + try { + traffic_light_http_server_ = std::make_unique( + system_config.traffic_light_server.port, + system_config.traffic_light_server.max_connections, + ".", + *this + ); + // Start server in its own thread (start itself handles creating internal threads for io_context) + traffic_light_http_server_thread_ = std::thread([this]() { + try { + traffic_light_http_server_->start(2); // Use 2 threads for IO context + } catch (const std::exception& e) { + Logger::error("TrafficLightHttpServer thread failed to start: ", e.what()); + } + }); + Logger::info("Traffic light HTTP server initialized on port ", system_config.traffic_light_server.port); + } catch (const std::exception& e) { + Logger::error("Failed to initialize Traffic Light HTTP Server: ", e.what()); + // Decide if this failure is critical + return false; // Example: treat as critical + } + + // 加载机场区域配置 + airportBounds_ = std::make_unique("config/airport_bounds.json"); + + // 初始化冲突检测器 + collisionDetector_ = std::make_unique(*airportBounds_, controllableVehicles_); + + // 初始化红绿灯检测器 + trafficLightDetector_ = std::make_unique(intersection_config_, controllableVehicles_, *this); + + // 初始化安全区 + initializeSafetyZones(); + + // 加载告警配置 + WarnConfig warnConfig{ + system_config.warning.warning_interval_ms, + system_config.warning.log_interval_ms }; + + // 创建数据采集器并初始化 + dataCollector_ = std::make_unique(*airportBounds_); + return dataCollector_->initialize(system_config.data_source, warnConfig); + } catch (const std::exception& e) { + Logger::error("Failed to initialize system: ", e.what()); + return false; + } +} + +void System::initializeSafetyZones() { + // 清空现有的安全区 + safetyZones_.clear(); + + // 获取所有路口配置 + const auto& intersections = intersection_config_.getIntersections(); + + // 为每个路口创建安全区 + for (const auto& intersection : intersections) { + // 获取路口中心点的地理坐标 + Vector2D center = CoordinateConverter::instance().toLocalXY( + intersection.position.latitude, + intersection.position.longitude); + + // 创建安全区 + auto safetyZone = std::make_unique( + center, + intersection.safetyZone.aircraftRadius, // 飞机安全区半径 + intersection.safetyZone.vehicleRadius // 特勤车安全区半径 + ); + + Logger::debug("创建路口安全区: id=", intersection.id, + ", center=(", center.x, ",", center.y, ")", + ", aircraftRadius=", intersection.safetyZone.aircraftRadius, + ", vehicleRadius=", intersection.safetyZone.vehicleRadius); + + // 保存安全区 + safetyZones_[intersection.id] = std::move(safetyZone); + } +} + +void System::start() { + if (running_) { + return; + } + + running_ = true; + dataCollector_->start(); + processThread_ = std::thread(&System::processLoop, this); + Logger::info("System processing loop started"); +} + +void System::stop() { + Logger::info("Stopping system..."); + running_ = false; + + // Stop data collector first + if (dataCollector_) { + dataCollector_->stop(); + } + + // Stop WebSocket server + if (ws_server_) { + ws_server_->stop(); + } + if (ws_thread_.joinable()) { + ws_thread_.join(); + } + + // 新增: Stop Traffic Light HTTP Server + if (traffic_light_http_server_) { + traffic_light_http_server_->stop(); + } + if (traffic_light_http_server_thread_.joinable()) { + traffic_light_http_server_thread_.join(); + } + + // Stop processing loop + if (processThread_.joinable()) { + processThread_.join(); + } + + Logger::info("System fully stopped."); +} + +void System::processLoop() { + while (running_) { + try { + auto now = std::chrono::steady_clock::now(); + + // 获取最新数据和时间戳 + std::tie(latest_aircraft_, latest_vehicles_, latest_traffic_lights_) = dataCollector_->getLatestData(); + auto [aircraft_ts, vehicle_ts, traffic_light_ts] = dataCollector_->getLastUpdateTimestamps(); + + // 检查数据更新 + bool has_new_data = false; + + if (aircraft_ts > last_aircraft_timestamp) { + has_new_data = true; + last_aircraft_timestamp = aircraft_ts; + Logger::debug("航空器数据已更新,新时间戳: ", aircraft_ts); + } + + if (vehicle_ts > last_vehicle_timestamp) { + has_new_data = true; + last_vehicle_timestamp = vehicle_ts; + Logger::debug("车辆数据已更新,新时间戳: ", vehicle_ts); + } + + if (traffic_light_ts > last_traffic_light_timestamp) { + has_new_data = true; + last_traffic_light_timestamp = traffic_light_ts; + Logger::debug("红绿灯数据已更新,新时间戳: ", traffic_light_ts); + } + + if (has_new_data) { + // 使用成员变量的引用构建 objects 向量 + std::vector objects; + for (auto& ac : latest_aircraft_) { + objects.push_back(&ac); + } + for (auto& veh : latest_vehicles_) { + const auto* config = controllableVehicles_.findVehicle(veh.vehicleNo); + if (config) { + veh.type = config->type == "UNMANNED" ? MovingObjectType::UNMANNED : MovingObjectType::SPECIAL; + veh.isControllable = config->type == "UNMANNED"; + } + objects.push_back(&veh); + } + + // 创建当前周期的风险管理器 + std::unordered_map vehicleMaxRiskLevels; // 统一记录所有车辆的最高风险等级 + std::vector detectedRisks; // 统一收集所有检测到的风险 + + // 检查安全区更新 + auto safety_zone_elapsed = std::chrono::duration_cast( + now - last_safety_zone_update) + .count(); + + if (safety_zone_elapsed >= SystemConfig::instance().collision_detection.update_interval_ms) { + updateSafetyZoneStates(objects); + Logger::debug("开始检查安全区冲突..."); + + // 记录安全区风险检测前的风险数量 + size_t beforeSafetyZone = detectedRisks.size(); + checkUnmannedVehicleSafetyZones(latest_vehicles_, objects, vehicleMaxRiskLevels, detectedRisks); + Logger::debug("安全区检测新增风险数量: ", detectedRisks.size() - beforeSafetyZone); + + last_safety_zone_update = now; + } + + // 检查和处理常规碰撞风险 + collisionDetector_->updateTraffic(latest_aircraft_, latest_vehicles_); + auto collisionRisks = collisionDetector_->detectCollisions(); + + Logger::debug("碰撞检测器检测到风险数量: ", collisionRisks.size()); + for (const auto& risk : collisionRisks) { + Logger::debug("碰撞风险: id1=", risk.id1, + ", id2=", risk.id2, + ", level=", static_cast(risk.level), + ", distance=", risk.distance); + } + + // 合并风险 + size_t beforeMerge = detectedRisks.size(); + detectedRisks.insert(detectedRisks.end(), collisionRisks.begin(), collisionRisks.end()); + Logger::debug("合并后新增风险数量: ", detectedRisks.size() - beforeMerge); + + // 统一处理所有风险 + processCollisions(detectedRisks, vehicleMaxRiskLevels); + + // 广播位置更新 + for (const auto& ac : latest_aircraft_) { + broadcastPositionUpdate(ac); + } + for (const auto& veh : latest_vehicles_) { + broadcastPositionUpdate(veh); + } + + // 处理红绿灯信号 + for (const auto& signal : latest_traffic_lights_) { + broadcastTrafficLightStatus(signal); + trafficLightDetector_->processSignal(signal, latest_vehicles_); + } + } + + // 等待下一次处理 + std::this_thread::sleep_for(std::chrono::milliseconds( + SystemConfig::instance().collision_detection.update_interval_ms)); + } catch (const std::exception& e) { + Logger::error("处理循环发生错误: ", e.what()); + } + } +} + +void System::checkUnmannedVehicleSafetyZones( + const std::vector& vehicles, + const std::vector& objects, + std::unordered_map& vehicleMaxRiskLevels, + std::vector& detectedRisks) { + + // 遍历所有无人车 + for (const auto& vehicle : vehicles) { + // 遍历所有路口安全区 + for (const auto& [intersectionId, zone] : safetyZones_) { + if (zone->getState() == SafetyZoneState::INACTIVE) { + continue; + } + + // 计算无人车到安全区中心的距离 + double dx = vehicle.position.x - zone->getCenter().x; + double dy = vehicle.position.y - zone->getCenter().y; + double distance = std::sqrt(dx * dx + dy * dy); + double zoneRadius = zone->getCurrentRadius(); + + RiskLevel riskLevel = RiskLevel::NONE; + if (distance <= zoneRadius) { + riskLevel = RiskLevel::CRITICAL; + } else if (distance <= 2 * zoneRadius) { + riskLevel = RiskLevel::WARNING; + } + + if (riskLevel != RiskLevel::NONE) { + // 查找触发安全区的目标物体 + for (const auto* obj : objects) { + // 添加判断:跳过与自己的碰撞检测 + if (obj->id == vehicle.id) { + continue; + } + + if ((obj->isAircraft() || obj->isSpecialVehicle()) && + zone->isObjectInZone(*obj)) { + // 创建风险对象 + CollisionRisk risk; + risk.id1 = vehicle.id; + risk.id2 = obj->id; + risk.level = riskLevel; + risk.distance = distance; + risk.minDistance = distance; + risk.relativeSpeed = std::sqrt( + std::pow(obj->speed * std::cos(obj->heading) - + vehicle.speed * std::cos(vehicle.heading), + 2) + + std::pow(obj->speed * std::sin(obj->heading) - + vehicle.speed * std::sin(vehicle.heading), + 2)); + risk.relativeMotion = { + obj->position.x - vehicle.position.x, + obj->position.y - vehicle.position.y }; + + detectedRisks.push_back(risk); + + // 更新风险级别 + auto& currentRisk = vehicleMaxRiskLevels[vehicle.id]; + currentRisk = std::max(currentRisk, riskLevel); + + Logger::debug("安全区风险: id1=", risk.id1, + ", id2=", risk.id2, + ", level=", static_cast(risk.level), + ", distance=", risk.distance); + + break; + } + } + } + } + } +} + +void System::processCollisions( + const std::vector& detectedRisks, + std::unordered_map& vehicleMaxRiskLevels) { + + // 记录当前有风险的可控车辆 + std::unordered_set currentVehiclesWithRisk; + + // 修改日志输出,显示更多信息 + Logger::debug("开始处理碰撞风险,风险数量: ", detectedRisks.size()); + for (const auto& risk : detectedRisks) { + Logger::debug("待处理风险: id1=", risk.id1, + ", id2=", risk.id2, + ", level=", static_cast(risk.level), + ", distance=", risk.distance, + ", minDistance=", risk.minDistance); + } + + // 处理当前的碰撞风险 + for (const auto& risk : detectedRisks) { + // 处理 id1 和 id2 中的可控车辆 + bool id1_controllable = controllableVehicles_.isControllable(risk.id1); + bool id2_controllable = controllableVehicles_.isControllable(risk.id2); + + auto processVehicle = [&](const std::string& vehicleId, const std::string& otherId) { + // 发送指令 + VehicleCommand cmd; + cmd.vehicleId = vehicleId; + cmd.type = risk.level == RiskLevel::CRITICAL ? CommandType::ALERT : CommandType::WARNING; + cmd.reason = otherId.substr(0, 2) == "AC" ? CommandReason::AIRCRAFT_CROSSING : CommandReason::SPECIAL_VEHICLE; + cmd.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + // 设置目标位置 + const MovingObject* target = findVehicle(otherId); + if (target) { + cmd.latitude = target->geo.latitude; + cmd.longitude = target->geo.longitude; + cmd.relativeSpeed = risk.relativeSpeed; + cmd.relativeMotionX = risk.relativeMotion.x; + cmd.relativeMotionY = risk.relativeMotion.y; + cmd.minDistance = risk.minDistance; + } + + Logger::debug("准备发送指令: 车辆=", vehicleId, + ", 类型=", cmd.type == CommandType::ALERT ? "ALERT" : "WARNING", + ", 原因=", cmd.reason == CommandReason::AIRCRAFT_CROSSING ? "AIRCRAFT_CROSSING" : "SPECIAL_VEHICLE", + ", 距离=", risk.distance, + ", 风险等级=", static_cast(risk.level)); + + broadcastVehicleCommand(cmd); + controllableVehicles_.sendCommand(vehicleId, cmd); + + // 更新风险记录 + currentVehiclesWithRisk.insert(vehicleId); + vehicleMaxRiskLevels[vehicleId] = risk.level; + + Logger::info("碰撞风险: 车辆=", vehicleId, + ", 目标=", otherId, + ", 距离=", risk.distance, "m", + ", 最小距离=", risk.minDistance, "m", + ", 相对速度=", risk.relativeSpeed, "m/s"); + }; + + // 为每个可控车辆处理风险 + if (id1_controllable) { + processVehicle(risk.id1, risk.id2); + } + if (id2_controllable) { + processVehicle(risk.id2, risk.id1); + } + + // 广播碰撞预警消息 + broadcastCollisionWarning(risk); + } + + // 处理恢复指令 + for (const auto& vehicleId : lastVehiclesWithRisk_) { + if (currentVehiclesWithRisk.find(vehicleId) == currentVehiclesWithRisk.end()) { + auto riskIt = vehicleMaxRiskLevels.find(vehicleId); + if (riskIt == vehicleMaxRiskLevels.end() || riskIt->second == RiskLevel::NONE) { + VehicleCommand cmd; + cmd.vehicleId = vehicleId; + cmd.type = CommandType::RESUME; + cmd.reason = CommandReason::RESUME_TRAFFIC; + cmd.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + broadcastVehicleCommand(cmd); + controllableVehicles_.sendCommand(vehicleId, cmd); + Logger::info("发送恢复指令到车辆: ", vehicleId); + } + } + } + + // 更新风险车辆列表 + lastVehiclesWithRisk_ = std::move(currentVehiclesWithRisk); +} + +void System::broadcastPositionUpdate(const MovingObject& obj) { + if (!ws_server_) { + return; + } + + network::PositionUpdateMessage msg; + msg.objectId = obj.id; + msg.objectType = (typeid(obj) == typeid(Aircraft)) ? "aircraft" : "vehicle"; + msg.timestamp = obj.timestamp; + msg.longitude = obj.geo.longitude; + msg.latitude = obj.geo.latitude; + msg.heading = obj.heading; + msg.speed = obj.speed; + + nlohmann::json j = { + {"type", "position_update"}, + {"object_id", msg.objectId}, + {"object_type", msg.objectType}, + {"timestamp", msg.timestamp}, + {"position", {{"longitude", msg.longitude}, {"latitude", msg.latitude}}}, + {"heading", msg.heading}, + {"speed", msg.speed} }; + + ws_server_->broadcast(j.dump()); + Logger::debug("广播位置更新: id=", msg.objectId, + " type=", msg.objectType, + " pos=(", msg.longitude, ",", msg.latitude, ")", + " heading=", msg.heading, + " speed=", msg.speed, + " timestamp=", msg.timestamp); +} + +// 新增辅助函数:将 SignalStatus 转换为字符串 +std::string signalStatusToString(SignalStatus status) { + switch (status) { + case SignalStatus::RED: return "RED"; + case SignalStatus::GREEN: return "GREEN"; + case SignalStatus::YELLOW: return "YELLOW"; + default: return "UNKNOWN"; + } +} + +void System::broadcastTrafficLightStatus(const TrafficLightSignal& signal) { + if (!ws_server_) { + return; + } + + // 查找路口信息 + const Intersection* intersection = intersection_config_.findByTrafficLightId(signal.trafficLightId); + + nlohmann::json j = { + {"type", "traffic_light_status"}, + {"id", signal.trafficLightId}, + // 修改: 将 status 改为一个包含 ns 和 ew 状态的对象 + {"status", { + {"ns", signalStatusToString(signal.ns_status)}, + {"ew", signalStatusToString(signal.ew_status)} + }}, + {"timestamp", signal.timestamp} + }; + + // 添加路口信息 + if (intersection) { + j["intersection"] = intersection->id; + j["position"] = { + {"longitude", intersection->position.longitude}, + {"latitude", intersection->position.latitude} }; + } + + ws_server_->broadcast(j.dump()); + + // 修改日志记录 + Logger::debug("广播红绿灯状态: id=", signal.trafficLightId, + ", NS_Status=", signalStatusToString(signal.ns_status), + ", EW_Status=", signalStatusToString(signal.ew_status), + ", intersection=", intersection ? intersection->id : "", + ", timestamp=", signal.timestamp); +} + +std::string getRiskLevelString(RiskLevel level) { + switch (level) { + case RiskLevel::CRITICAL: + return "critical"; + case RiskLevel::WARNING: + return "warning"; + default: + return "none"; + } +} + +void System::broadcastCollisionWarning(const CollisionRisk& risk) { + if (!ws_server_) { + return; + } + + // 构造冲突预警消息 + nlohmann::json j = { + {"type", "collision_warning"}, + {"id1", risk.id1}, + {"id2", risk.id2}, + {"distance", risk.distance}, + {"relativeSpeed", risk.relativeSpeed}, + {"warningLevel", getRiskLevelString(risk.level)}, + {"timestamp", std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()} }; + + // 广播冲突预警消息 + ws_server_->broadcast(j.dump()); + Logger::debug("广播冲突预警: id1=", risk.id1, " id2=", risk.id2, + " distance=", risk.distance, "m", + " relativeSpeed=", risk.relativeSpeed, "m/s", + " level=", getRiskLevelString(risk.level)); +} + +void System::broadcastTimeoutWarning(const network::TimeoutWarningMessage& warning) { + if (ws_server_) { + ws_server_->broadcast(warning.toJson().dump()); + + // 根据超时时间记录不同级的日志 + if (warning.elapsed_ms > 30000) { // 30秒以上 + Logger::error("Severe timeout: ", warning.source, " - ", + warning.elapsed_ms, "ms without response"); + } else { + Logger::warning("Connection timeout: ", warning.source, " - ", + warning.elapsed_ms, "ms without response"); + } + } +} + +void System::broadcastVehicleCommand(const VehicleCommand& cmd) { + if (!ws_server_) { + return; + } + + nlohmann::json j = { + {"type", "vehicle_command"}, + {"vehicleId", cmd.vehicleId}, + {"commandType", [&]() { + switch (cmd.type) { + case CommandType::SIGNAL: + return "SIGNAL"; + case CommandType::ALERT: + return "ALERT"; + case CommandType::WARNING: + return "WARNING"; + case CommandType::RESUME: + return "RESUME"; + case CommandType::PARKING: + return "PARKING"; + default: + return "UNKNOWN"; + } + }()}, + {"reason", [&]() { + switch (cmd.reason) { + case CommandReason::TRAFFIC_LIGHT: + return "TRAFFIC_LIGHT"; + case CommandReason::AIRCRAFT_CROSSING: + return "AIRCRAFT_CROSSING"; + case CommandReason::SPECIAL_VEHICLE: + return "SPECIAL_VEHICLE"; + case CommandReason::AIRCRAFT_PUSH: + return "AIRCRAFT_PUSH"; + case CommandReason::RESUME_TRAFFIC: + return "RESUME_TRAFFIC"; + case CommandReason::PARKING_SIDE: + return "PARKING_SIDE"; + default: + return "UNKNOWN"; + } + }()}, + {"timestamp", cmd.timestamp} }; + + // 添加可选字段 + if (cmd.type == CommandType::SIGNAL) { + j["signalState"] = cmd.signalState == SignalState::RED ? "RED" : "GREEN"; + j["intersectionId"] = cmd.intersectionId; + } + + // 添目标位置(对于所有非 RESUME 类型的指令) + if (cmd.type != CommandType::RESUME) { + j["targetLatitude"] = cmd.latitude; + j["targetLongitude"] = cmd.longitude; + } + + ws_server_->broadcast(j.dump()); +} + +const MovingObject* System::findVehicle(const std::string& vehicleId) const { + if (!dataCollector_) { + return nullptr; + } + + // 获取数据引用(注意:DataCollector 内部会加锁) + const auto& aircrafts = dataCollector_->getAircraftRef(); + const auto& vehicles = dataCollector_->getVehicleRef(); + + // 在航空器中查找 + for (const auto& aircraft : aircrafts) { + if (aircraft.flightNo == vehicleId) { + return &aircraft; + } + } + + // 在车辆中查找 + for (const auto& vehicle : vehicles) { + if (vehicle.vehicleNo == vehicleId) { + return &vehicle; + } + } + + return nullptr; +} + +void System::updateSafetyZoneStates(const std::vector& objects) { + // 重置所有安全区状态为未激活,同时重置类型 + for (auto& [id, zone] : safetyZones_) { + zone->setState(SafetyZoneState::INACTIVE); + zone->resetType(); // 重置安全区类型 + } + + // 检查所有移动物体 + for (const auto& obj : objects) { + Logger::debug("检查移动物体: id=", obj->id, + ", type=", obj->type == MovingObjectType::UNMANNED ? "无人车" : (obj->type == MovingObjectType::SPECIAL ? "特勤车" : "飞机"), + ", isAircraft=", obj->isAircraft(), + ", isSpecialVehicle=", obj->isSpecialVehicle(), + ", isUnmannedVehicle=", obj->isUnmannedVehicle()); + + // 只检查飞机和特勤车 + if (obj->isAircraft() || obj->isSpecialVehicle()) { + checkSafetyZoneIntrusion(*obj); + } + } +} + +void System::checkSafetyZoneIntrusion(const MovingObject& obj) { + Logger::debug("检查安全区入侵: id=", obj.id, + ", type=", obj.isAircraft() ? "飞机" : (obj.isSpecialVehicle() ? "特勤车" : "其他"), + ", position=(", obj.position.x, ",", obj.position.y, ")"); + + // 检查每个安全区 + for (auto& [id, zone] : safetyZones_) { + // 先检查是否在安全区内 + if (zone->isObjectInZone(obj)) { + // 如果在区内,尝试激活安全区 + if (zone->tryActivate(obj)) { + Logger::debug("目标 ", obj.id, " 进入路口 ", id, " 安全区, 类型: ", + zone->getType() == SafetyZoneType::AIRCRAFT ? "飞机" : "特勤车", + ", 半径: ", zone->getCurrentRadius(), + ", 中心点=(", zone->getCenter().x, ",", zone->getCenter().y, ")"); + } + } + } +} + +bool System::handleSafetyZoneRisk(const Vehicle& vehicle, + const SafetyZone* zone, + const std::vector& objects, + double distance, + const std::string& intersectionId, + CommandType cmdType, + const std::string& riskLevel) { + // 检查是否为无人车 + if (!controllableVehicles_.isControllable(vehicle.id)) { + return false; + } + + VehicleCommand cmd; + cmd.vehicleId = vehicle.id; + cmd.type = cmdType; + cmd.reason = zone->getType() == SafetyZoneType::AIRCRAFT ? CommandReason::AIRCRAFT_CROSSING : CommandReason::SPECIAL_VEHICLE; + cmd.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + // 查找触发安全区的目标物体 + const MovingObject* target = nullptr; + for (const auto& obj : objects) { + if ((obj->isAircraft() || obj->isSpecialVehicle()) && + zone->isObjectInZone(*obj)) { + target = obj; + break; + } + } + + // 如果找到目标物体,添加相关信息 + if (target) { + cmd.latitude = target->geo.latitude; + cmd.longitude = target->geo.longitude; + + // 计算相对距离 + double rel_dx = target->position.x - vehicle.position.x; + double rel_dy = target->position.y - vehicle.position.y; + + // 计算相对速度 + cmd.relativeSpeed = std::sqrt( + (target->position.x - vehicle.position.x) * (target->position.x - vehicle.position.x) + + (target->position.y - vehicle.position.y) * (target->position.y - vehicle.position.y)); + cmd.relativeMotionX = rel_dx; + cmd.relativeMotionY = rel_dy; + cmd.minDistance = distance; + + Logger::debug("安全区冲突目标信息: id=", target->id, + ", 相对距离=(", rel_dx, ",", rel_dy, ")", + ", 相对速度=", cmd.relativeSpeed, + ", 最小距离=", distance); + } + + broadcastVehicleCommand(cmd); + controllableVehicles_.sendCommand(vehicle.id, cmd); + + CollisionRisk risk; + risk.id1 = vehicle.id; + risk.id2 = target ? target->id : ""; + risk.level = cmdType == CommandType::ALERT ? RiskLevel::CRITICAL : RiskLevel::WARNING; + risk.distance = distance; + risk.relativeSpeed = cmd.relativeSpeed; + risk.relativeMotion = { cmd.relativeMotionX, cmd.relativeMotionY }; + risk.zoneType = WarningZoneType::WARNING; + + broadcastCollisionWarning(risk); + + return true; +} + +void System::processPushedTrafficLightData(const nlohmann::json& di_data) { + try { + const auto& config = SystemConfig::instance(); + const std::string targetIntersectionId = config.simulated_mobile_light_target_intersection_id; + const Intersection* targetIntersection = intersection_config_.findById(targetIntersectionId); + + if (!targetIntersection) { + Logger::warning("Configured target intersection ID not found in intersection config: ", targetIntersectionId); + return; + } + + SignalStatus ns_status = SignalStatus::UNKNOWN; + SignalStatus ew_status = SignalStatus::UNKNOWN; + + if (di_data.contains("DI-13") && di_data.at("DI-13").get() == 1) { + ns_status = SignalStatus::GREEN; + } else if (di_data.contains("DI-12") && di_data.at("DI-12").get() == 1) { + ns_status = SignalStatus::YELLOW; + } else if (di_data.contains("DI-11") && di_data.at("DI-11").get() == 1) { + ns_status = SignalStatus::RED; + } else { + Logger::warning("Received DI data has unknown or invalid N/S state: ", di_data.dump()); + } + + if (di_data.contains("DI-16") && di_data.at("DI-16").get() == 1) { + ew_status = SignalStatus::GREEN; + } else if (di_data.contains("DI-15") && di_data.at("DI-15").get() == 1) { + ew_status = SignalStatus::YELLOW; + } else if (di_data.contains("DI-14") && di_data.at("DI-14").get() == 1) { + ew_status = SignalStatus::RED; + } else { + Logger::warning("Received DI data has unknown or invalid E/W state: ", di_data.dump()); + } + + TrafficLightSignal signal; + signal.trafficLightId = "DI_PUSHED_" + targetIntersectionId; + signal.ns_status = ns_status; + signal.ew_status = ew_status; + signal.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + if (ws_server_) { + nlohmann::json messageJson; + messageJson["type"] = "intersection_traffic_light_status"; + messageJson["intersection_id"] = targetIntersection->id; + messageJson["position"] = { + {"latitude", targetIntersection->position.latitude}, + {"longitude", targetIntersection->position.longitude} + }; + messageJson["ns_status"] = static_cast(signal.ns_status); + messageJson["ew_status"] = static_cast(signal.ew_status); + messageJson["timestamp"] = signal.timestamp; + + ws_server_->broadcast(messageJson.dump()); + Logger::debug("广播路口交通灯状态: ", messageJson.dump()); + } else { + Logger::warning("WebSocket server is not available, cannot broadcast intersection status."); + } + + } catch (const std::exception& e) { + Logger::error("处理推送的交通信号灯数据时出错: ", e.what()); + } +} \ No newline at end of file diff --git a/src/core/System.h b/src/core/System.h new file mode 100644 index 0000000..ff4ee52 --- /dev/null +++ b/src/core/System.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "types/BasicTypes.h" +#include "detector/CollisionDetector.h" +#include "detector/TrafficLightDetector.h" +#include "detector/SafetyZone.h" +#include "config/AirportBounds.h" +#include "vehicle/ControllableVehicles.h" +#include "network/WebSocketServer.h" +#include "network/MessageTypes.h" +#include "config/IntersectionConfig.h" +#include "network/TrafficLightHttpServer.h" +#include + +// 前向声明 +class DataCollector; +class SystemConfig; // 添加前向声明 + +class System { +public: + System(); + virtual ~System(); + + bool initialize(); + void start(); + void stop(); + + static System* instance() { return instance_; } + static void signalHandler(int signal); + + // 广播消息给客户端 + virtual void broadcastTimeoutWarning(const network::TimeoutWarningMessage& warning); + void broadcastPositionUpdate(const MovingObject& obj); + void broadcastCollisionWarning(const CollisionRisk& risk); + void broadcastVehicleCommand(const VehicleCommand& cmd); + void broadcastTrafficLightStatus(const TrafficLightSignal& signal); + + const SystemConfig& getSystemConfig() const { return SystemConfig::instance(); } + const IntersectionConfig& getIntersectionConfig() const { return intersection_config_; } + + // 新增: 处理推送的红绿灯数据 (改回接收 json 对象) + void processPushedTrafficLightData(const nlohmann::json& di_data); + + // 处理来自数据源的交通信号灯状态 + void processTrafficLightStatus(const TrafficLightSignal& signal); + +private: + void processLoop(); + void processCollisions(const std::vector& detectedRisks, + std::unordered_map& vehicleMaxRiskLevels); + + // 初始化安全区 + void initializeSafetyZones(); + + // 更新安全区状态 + void updateSafetyZoneStates(const std::vector& objects); + + // 检查目标是否进入安全区 + void checkSafetyZoneIntrusion(const MovingObject& obj); + + // 检查无人车与安全区的冲突 + void checkUnmannedVehicleSafetyZones(const std::vector& vehicles, + const std::vector& objects, + std::unordered_map& vehicleMaxRiskLevels, + std::vector& detectedRisks); + + // 处理安全区风险 + bool handleSafetyZoneRisk(const Vehicle& vehicle, + const SafetyZone* zone, + const std::vector& objects, + double distance, + const std::string& intersectionId, + CommandType cmdType, + const std::string& riskLevel); + + // 记录上一次有安全区风险的无人车 + std::unordered_map lastVehiclesWithSafetyZoneRisk_; // + + std::atomic running_{false}; + std::thread processThread_; + + std::unique_ptr airportBounds_; + ControllableVehicles& controllableVehicles_{ControllableVehicles::getInstance()}; + std::unique_ptr collisionDetector_; + std::unique_ptr trafficLightDetector_; + std::unique_ptr dataCollector_; + + // WebSocket 服务器 + std::unique_ptr ws_server_; + std::thread ws_thread_; + + // 新增: 红绿灯 HTTP 服务器 + std::unique_ptr traffic_light_http_server_; + std::thread traffic_light_http_server_thread_; + + // 路口配置 + IntersectionConfig intersection_config_; + + // 安全区管理 + std::unordered_map> safetyZones_; + + static System* instance_; + + // 记录上一次有风险的车辆列表 + std::unordered_set lastVehiclesWithRisk_; + + // 时间戳 + int64_t last_aircraft_timestamp{0}; + int64_t last_vehicle_timestamp{0}; + int64_t last_traffic_light_timestamp{0}; + std::chrono::steady_clock::time_point last_safety_zone_update; + + // 辅助函数 + const MovingObject* findVehicle(const std::string& vehicleId) const; + + // 存储最新数据的成员变量 + std::vector latest_aircraft_; + std::vector latest_vehicles_; + std::vector latest_traffic_lights_; +}; \ No newline at end of file diff --git a/src/detector/CollisionDetector.cpp b/src/detector/CollisionDetector.cpp new file mode 100644 index 0000000..f938cb5 --- /dev/null +++ b/src/detector/CollisionDetector.cpp @@ -0,0 +1,729 @@ +#include "CollisionDetector.h" + +#include "config/SystemConfig.h" +#include "utils/Logger.h" +#include +#include + +CollisionDetector::CollisionDetector(const AirportBounds& bounds, const ControllableVehicles& controllableVehicles) + : airportBounds_(bounds), + vehicleTree_(bounds.getAirportBounds(), 8), // 使用机场总边界初始化四叉树 + controllableVehicles_(&controllableVehicles) { + // 记录初始化信息 + const auto& airportBounds = bounds.getAirportBounds(); +} + +void CollisionDetector::updateTraffic(const std::vector& aircraft, + const std::vector& vehicles) { + Logger::debug("更新交通数据: 航空器数量=", aircraft.size(), + ", 车辆数量=", vehicles.size()); + + // 更新航空器数据 + aircraftData_ = aircraft; + + // 更新车辆四叉树 + vehicleTree_.clear(); + size_t validVehicles = 0; + + for (const auto& vehicle : vehicles) { + // 首先检查车辆是否在机场边界内 + if (!airportBounds_.isPointInBounds(vehicle.position)) { + Logger::error("车辆在机场边界外: id=", vehicle.id, ", position=(", + vehicle.position.x, ",", vehicle.position.y, ")"); + continue; // 跳过边界外的车辆 + } + + // 根据配置设置车辆类型 + Vehicle updatedVehicle = vehicle; + const auto* config = + controllableVehicles_->findVehicle(vehicle.vehicleNo); + if (config) { + if (config->type == "UNMANNED") { + updatedVehicle.type = MovingObjectType::UNMANNED; + updatedVehicle.isControllable = true; + } else if (config->type == "SPECIAL") { + updatedVehicle.type = MovingObjectType::SPECIAL; + updatedVehicle.isControllable = false; + } + } else { + updatedVehicle.type = MovingObjectType::SPECIAL; + updatedVehicle.isControllable = false; + } + + // 插入四叉树 + try { + Logger::debug("尝试插入车辆: id=", updatedVehicle.id, + ", position=(", updatedVehicle.position.x, ",", + updatedVehicle.position.y, ")", ", type=", + updatedVehicle.isControllable ? "UNMANNED" + : "SPECIAL"); + vehicleTree_.insert(updatedVehicle); + ++validVehicles; + Logger::debug("成功插入车辆: id=", updatedVehicle.id); + } catch (const std::exception& e) { + Logger::error("无法插入车辆到四叉树: id=", updatedVehicle.id, + ", error=", e.what()); + } + } + + // 记录更新后的状态 + auto bounds = vehicleTree_.getBounds(); + Logger::debug("四叉树边界: min=(", bounds.x, ",", bounds.y, "), max=(", + bounds.x + bounds.width, ",", bounds.y + bounds.height, ")"); + auto allVehicles = vehicleTree_.queryRange(bounds); + for (const auto& vehicle : allVehicles) { + Logger::debug( + "查询到车辆: id=", vehicle.id, ", position=(", vehicle.position.x, + ",", vehicle.position.y, ")", + ", type=", vehicle.isControllable ? "UNMANNED" : "SPECIAL"); + } + Logger::debug("交通数据更新完成: 有效车辆数量=", validVehicles, + ", 四叉树中的车辆数量=", allVehicles.size()); +} + +std::vector CollisionDetector::detectCollisions() { + std::vector risks; + + // 获取配置参数 + const auto& predictionConfig = + SystemConfig::instance().collision_detection.prediction; + + // 获取所有车辆 + auto allVehicles = vehicleTree_.queryRange(vehicleTree_.getBounds()); + + // 可控车辆(无人车) + std::vector unmannedVehicles; + for (const auto& vehicle : allVehicles) { + if (vehicle.isControllable) { + unmannedVehicles.push_back(vehicle); + } + } + + // 记录当前检测到的冲突对 + std::unordered_set, CollisionPairHash> + currentCollisions; + + // 检测可控车辆与航空器的碰撞 + for (const auto& aircraft : aircraftData_) { + for (const auto& vehicle : unmannedVehicles) { + // 获取冲突记录键 + auto collisionKey = getCollisionKey(aircraft.id, vehicle.id); + + // 获取区域配置 + const auto& areaConfig = getCollisionParams(aircraft.position); + double safeDistance = areaConfig.warning_zone_radius.aircraft + + areaConfig.warning_zone_radius.unmanned; + + // 检查是否存在未解除的冲突记录 + auto it = collisionRecords_.find(collisionKey); + bool hasUnresolvedConflict = + (it != collisionRecords_.end() && !it->second.resolved); + + if (hasUnresolvedConflict) { + Logger::debug("存在未解除的冲突记录: obj1=", aircraft.id, + ", obj2=", vehicle.id, ", maxLevel=", + static_cast(it->second.maxLevel), + ", collisionPoint=(", it->second.collisionPoint.x, + ",", it->second.collisionPoint.y, ")"); + } + + // 检测碰撞 + auto collisionResult = + checkCollision(aircraft, vehicle, predictionConfig.time_window); + + // 计算相对运动 + MovementVector av(aircraft.speed, aircraft.heading); + MovementVector vv(vehicle.speed, vehicle.heading); + double vx = av.vx - vv.vx; + double vy = av.vy - vv.vy; + double relativeSpeed = std::sqrt(vx * vx + vy * vy); + + // 根据相对运动方向调整相对速度符号 + if (collisionResult.timeToCollision > 0) { + relativeSpeed = -relativeSpeed; + } + + // 评估风险等级 + auto [level, zoneType] = + evaluateRisk(collisionResult, aircraft.position, aircraft.type, + vehicle.type); + + if (level != RiskLevel::NONE) { + // 记录或更新冲突信息 + auto& record = collisionRecords_[collisionKey]; + if (!record.resolved) { + // 更新已存在的冲突记录 + record.maxLevel = std::max(record.maxLevel, level); + record.collisionPoint = + collisionResult.collisionPoint; // 更新冲突点 + Logger::debug("更新冲突记录: obj1=", aircraft.id, + ", obj2=", vehicle.id, ", maxLevel=", + static_cast(record.maxLevel), + ", collisionPoint=(", record.collisionPoint.x, + ",", record.collisionPoint.y, ")"); + } else { + // 创建新的冲突记录 + record = { collisionResult + .collisionPoint, // 使用当前检测到的冲突点 + std::chrono::system_clock::now() + .time_since_epoch() + .count(), + level, false }; + Logger::debug("创建新的冲突记录: obj1=", aircraft.id, + ", obj2=", vehicle.id, + ", level=", static_cast(level), + ", collisionPoint=(", + collisionResult.collisionPoint.x, ",", + collisionResult.collisionPoint.y, ")"); + } + + // 添加到当前冲突集合 + currentCollisions.insert(collisionKey); + + // 添加到风险列表 + risks.push_back({ aircraft.id, + vehicle.id, + level, + collisionResult.minDistance, + collisionResult.minDistance, + relativeSpeed, + {vx, vy}, + zoneType, + collisionResult.timeToCollision, + collisionResult.collisionPoint }); + + Logger::debug( + "检测到碰撞风险: obj1=", aircraft.id, ", obj2=", vehicle.id, + ", minDistance=", collisionResult.minDistance, "m", + ", timeToCollision=", collisionResult.timeToCollision, "s", + ", level=", static_cast(level)); + } else if (hasUnresolvedConflict) { + // 检查是否满足解除条件 + if (checkCollisionResolved(aircraft, vehicle, it->second, + safeDistance)) { + it->second.resolved = true; + Logger::debug("冲突已解除: obj1=", aircraft.id, + ", obj2=", vehicle.id); + } else { + // 虽然当前没有检测到风险,但由于未满足解除条件,继续保持原有风险等级 + currentCollisions.insert(collisionKey); + risks.push_back({ aircraft.id, + vehicle.id, + it->second.maxLevel, + collisionResult.minDistance, + collisionResult.minDistance, + relativeSpeed, + {vx, vy}, + zoneType, + collisionResult.timeToCollision, + it->second.collisionPoint }); + Logger::debug( + "保持原有风险等级: obj1=", aircraft.id, + ", obj2=", vehicle.id, + ", maxLevel=", static_cast(it->second.maxLevel), + ", minDistance=", collisionResult.minDistance, "m", + ", timeToCollision=", collisionResult.timeToCollision, + "s"); + } + } + } + } + + // 清理已解除的冲突记录 + for (auto it = collisionRecords_.begin(); it != collisionRecords_.end();) { + if (it->second.resolved) { + it = collisionRecords_.erase(it); + } else { + ++it; + } + } + + return risks; +} + +bool CollisionDetector::checkCollisionResolved(const MovingObject& obj1, + const MovingObject& obj2, + const CollisionRecord& record, + double safeDistance) const { + // 计算航空器与冲突点的距离 + double dist1 = + std::sqrt(std::pow(obj1.position.x - record.collisionPoint.x, 2) + + std::pow(obj1.position.y - record.collisionPoint.y, 2)); + + double dist2 = + std::sqrt(std::pow(obj2.position.x - record.collisionPoint.x, 2) + + std::pow(obj2.position.y - record.collisionPoint.y, 2)); + + // 计算冲突点相对于航空器的位置向量与航空器航向的夹角 + double dx = + record.collisionPoint.x - obj1.position.x; // 冲突点相对于航空器的向量 + double dy = record.collisionPoint.y - obj1.position.y; + double pointAngle = + std::atan2(dy, dx) * 180.0 / M_PI; // 冲突点相对于航空器的角度 + double angleDiff = + normalizeAngle(pointAngle - (90.0 - obj1.heading)); // 修正航向角 + + Logger::debug("检查冲突解除条件: 航空器位置=(", obj1.position.x, ",", + obj1.position.y, "), 冲突点=(", record.collisionPoint.x, ",", + record.collisionPoint.y, "), 航向=", obj1.heading, + ", 冲突点角度=", pointAngle, ", 角度差=", angleDiff, + ", 距离=", dist1); + + // 判断航空器是否已经通过交叉点 + if (angleDiff > 90 && angleDiff < 270) { + // 航空器已经通过交叉点,只要航空器离开冲突点的距离大于安全距离就可以解除 + Logger::debug("航空器已通过交叉点,航空器距离=", dist1, + "m, 安全距离=", safeDistance, "m"); + return dist1 >= safeDistance; + } else { + // 航空器还未通过交叉点,需要两者都在安全距离外且预计不会碰撞 + auto collisionResult = + checkCollision(obj1, obj2, 30.0); // 使用30秒的预测窗口 + bool willCollide = collisionResult.willCollide; + bool bothOutside = (dist1 >= safeDistance && dist2 >= safeDistance); + + Logger::debug("航空器未通过交叉点,航空器距离=", dist1, + "m, 无人车距离=", dist2, "m, 安全距离=", safeDistance, + "m, 预测碰撞=", willCollide ? "是" : "否"); + + return !willCollide && bothOutside; + } +} + +// 统一的风险评估函数 +std::pair CollisionDetector::evaluateRisk( + const CollisionResult& collisionResult, const Vector2D& position, + MovingObjectType type1, MovingObjectType type2) const { + + // 取区域配置 + const auto& areaConfig = getCollisionParams(position); + auto thresholds = areaConfig.getThresholds(type1, type2); + + RiskLevel level = RiskLevel::NONE; + WarningZoneType zoneType = WarningZoneType::NONE; + + // 如果预测到碰撞,根据当前距离判断风险等级 + if (collisionResult.willCollide) { + // 当前距离小于警报距离 -> 危险等级 + if (collisionResult.distance <= thresholds.critical) { + level = RiskLevel::CRITICAL; + zoneType = WarningZoneType::DANGER; + } + // 当前距离在预警范围内 -> 预警等级 + else if (collisionResult.distance <= thresholds.warning) { + level = RiskLevel::WARNING; + zoneType = WarningZoneType::WARNING; + } + } + + // 修改日志,添加物体类型信息 + Logger::debug("风险评估: 物体1类型=", static_cast(type1), + ", 物体2类型=", static_cast(type2), + ", 当前距离=", collisionResult.distance, + "m, 预测最小距离=", collisionResult.minDistance, + "m, 预警阈值=", thresholds.warning, + "m, 警报阈值=", thresholds.critical, + "m, 预测碰撞=", collisionResult.willCollide ? "是" : "否", + ", 风险等级=", static_cast(level), + ", 区域类型=", static_cast(zoneType)); + + return { level, zoneType }; +} + +// 统一的碰撞检测函数 +CollisionResult CollisionDetector::checkCollision(const MovingObject& obj1, + const MovingObject& obj2, + double timeWindow) const { + + // 添加物体信息日志 + Logger::debug("检查碰撞: obj1[", obj1.id, "]=(", obj1.position.x, ",", + obj1.position.y, "), obj2[", obj2.id, "]=(", obj2.position.x, + ",", obj2.position.y, ")"); + + // 获取区域配置 + const auto& areaConfig = getCollisionParams(obj1.position); + + // 获取各自的碰撞半径和警告半径 + double radius1 = areaConfig.collision_radius.getRadius(obj1.type); + double radius2 = areaConfig.collision_radius.getRadius(obj2.type); + double warning_radius1 = + areaConfig.warning_zone_radius.getRadius(obj1.type); + double warning_radius2 = + areaConfig.warning_zone_radius.getRadius(obj2.type); + + // 计算当前距离 + double dx = obj2.position.x - obj1.position.x; + double dy = obj2.position.y - obj1.position.y; + double current_distance = std::sqrt(dx * dx + dy * dy); + + // 如果是静止的无人车,使用基础速度 + double speed1_calc = obj1.speed; + double speed2_calc = obj2.speed; + + if (obj1.type == MovingObjectType::UNMANNED && speed1_calc < 0.1) { + speed1_calc = SystemConfig::instance() + .collision_detection.prediction + .min_unmanned_speed; // 使用配置的最低速度 + } + if (obj2.type == MovingObjectType::UNMANNED && speed2_calc < 0.1) { + speed2_calc = SystemConfig::instance() + .collision_detection.prediction + .min_unmanned_speed; // 使用配置的最低速度 + } + + // 计算速度分量 + double vx1 = + speed1_calc * std::cos((90.0 - obj1.heading) * M_PI / 180.0); // x 方向 + double vy1 = + speed1_calc * std::sin((90.0 - obj1.heading) * M_PI / 180.0); // y 方向 + double vx2 = speed2_calc * std::cos((90.0 - obj2.heading) * M_PI / 180.0); + double vy2 = speed2_calc * std::sin((90.0 - obj2.heading) * M_PI / 180.0); + + // 如果当前距离大于警告区域,直接返回不会碰撞 + if (current_distance > (warning_radius1 + warning_radius2)) { + CollisionResult result; + result.willCollide = false; + result.minDistance = current_distance; + result.distance = current_distance; + return result; + } + + // 否则进行碰撞检测 + return predictCircleBasedCollision(obj1.position, radius1, obj1.speed, + obj1.heading, obj2.position, radius2, + obj2.speed, obj2.heading, timeWindow); +} + +CollisionResult CollisionDetector::predictCircleBasedCollision( + const Vector2D& pos1, double radius1, double speed1, double heading1, + const Vector2D& pos2, double radius2, double speed2, double heading2, + double timeWindow) const { + + CollisionResult result; + + // 计算安全距离(两个圆的半径之和) + double safe_distance = radius1 + radius2; + + // 1. 计算当前距离 + double dx = pos2.x - pos1.x; + double dy = pos2.y - pos1.y; + double current_distance = std::sqrt(dx * dx + dy * dy); + + // 初始化最小距离和当前距离 + result.minDistance = current_distance; + result.distance = current_distance; + result.timeToMinDistance = 0.0; + + // 2. 计算速度分量 + double vx1 = speed1 * std::sin((90.0 - heading1) * M_PI / 180.0); + double vy1 = speed1 * std::cos((90.0 - heading1) * M_PI / 180.0); + double vx2 = speed2 * std::sin((90.0 - heading2) * M_PI / 180.0); + double vy2 = speed2 * std::cos((90.0 - heading2) * M_PI / 180.0); + + // 3. 计算相对运动 + double rel_vx = vx2 - vx1; + double rel_vy = vy2 - vy1; + double rel_speed = std::sqrt(rel_vx * rel_vx + rel_vy * rel_vy); + + // 计算相对运动方向与连线方向的夹角 + double motion_angle = std::atan2(rel_vy, rel_vx); + double connection_angle = std::atan2(dy, dx); + double angle = std::abs(motion_angle - connection_angle); + if (angle > M_PI) { + angle = 2 * M_PI - angle; // 确保夹角不超过180度 + } + + // 4. 确定碰撞类型 + if (speed1 < 0.1 && speed2 < 0.1) { + result.type = CollisionType::STATIC; + if (current_distance <= safe_distance) { + result.willCollide = true; + result.timeToCollision = 0.0; + result.minDistance = current_distance; + result.timeToMinDistance = 0.0; + result.collisionPoint = { (pos1.x + pos2.x) / 2.0, + (pos1.y + pos2.y) / 2.0 }; + result.object1State = CollisionObjectState(pos1, speed1, heading1); + result.object2State = CollisionObjectState(pos2, speed2, heading2); + } + return result; + } + + double angle_diff = getAngleDifference(heading1, heading2); + if (angle_diff > 150.0) { + result.type = CollisionType::HEAD_ON; + // 对于相向运动,直接计算碰撞时间 + double closing_speed = speed1 + speed2; // 相对接近速度是两个速度之和 + if (closing_speed > 0.1) { // 避免除以零 + double collision_distance = current_distance - safe_distance; + if (collision_distance <= 0) { + result.willCollide = true; + result.timeToCollision = 0.0; + result.minDistance = current_distance; + result.timeToMinDistance = 0.0; + result.collisionPoint = { (pos1.x + pos2.x) / 2.0, + (pos1.y + pos2.y) / 2.0 }; + result.object1State = + CollisionObjectState(pos1, speed1, heading1); + result.object2State = + CollisionObjectState(pos2, speed2, heading2); + } else { + result.timeToCollision = collision_distance / closing_speed; + result.willCollide = result.timeToCollision <= timeWindow; + + if (result.willCollide) { + result.minDistance = safe_distance; + result.timeToMinDistance = result.timeToCollision; + + // 计算碰撞时两个物体的实际位置 + double move_distance1 = speed1 * result.timeToCollision; + double move_distance2 = speed2 * result.timeToCollision; + + // 计算物体1的碰撞位置(向前移动到碰撞点前方) + Vector2D collision1 = { + pos1.x + + move_distance1 * std::sin(heading1 * M_PI / 180.0), + pos1.y + + move_distance1 * std::cos(heading1 * M_PI / 180.0) }; + + // 计算物体2的碰撞位置(也是向前移动) + Vector2D collision2 = { + pos2.x + + move_distance2 * std::sin(heading2 * M_PI / 180.0), + pos2.y + + move_distance2 * std::cos(heading2 * M_PI / 180.0) }; + + // 碰撞点两个物体的中点 + result.collisionPoint = { + (collision1.x + collision2.x) / 2.0, + (collision1.y + collision2.y) / 2.0 }; + + result.object1State = + CollisionObjectState(collision1, speed1, heading1); + result.object2State = + CollisionObjectState(collision2, speed2, heading2); + } + } + return result; + } + } else if (angle_diff > 15.0 && + angle_diff < 165.0) { // 放宽交叉路径的角度范围 + result.type = CollisionType::CROSSING; + + Logger::debug("交叉路径检测: angle_diff=", angle_diff, ", pos1=(", + pos1.x, ",", pos1.y, ")", ", pos2=(", pos2.x, ",", pos2.y, + ")", ", v1=(", vx1, ",", vy1, ")", ", v2=(", vx2, ",", + vy2, ")", ", current_distance=", current_distance, + ", safe_distance=", safe_distance); + + // 计算行列式 (v1 x v2) + double det = vx1 * (-vy2) - (-vx2) * vy1; + + // 计算 det1 = (P2-P1) x (-v2) + double det1 = dx * (-vy2) - (-vx2) * dy; + + // 计算 det2 = v1 x (P2-P1) + double det2 = vx1 * dy - dx * vy1; + + // 计算时间 + double t1 = det1 / det; // 物体1的时间 + double t2 = det2 / det; // 物体2的时间 + + Logger::debug("交叉路径检测: det1=", det1, ", det2=", det2, + ", det=", det, ", t1=", t1, ", t2=", t2); + + // 计算相交点 + Vector2D intersection = { pos1.x + vx1 * t1, pos1.y + vy1 * t1 }; + + Logger::debug("交叉路径检测: intersection=(", intersection.x, ",", + intersection.y, ")", ", t1=", t1, ", t2=", t2); + + // 检查时间条件 + double first_arrival_time = std::min(t1, t2); + double first_arrival_speed = (t1 < t2) ? speed1 : speed2; + double time_threshold = safe_distance / first_arrival_speed; + bool valid_time = + (t1 >= 0 && t2 >= 0 && std::max(t1, t2) <= timeWindow && + std::abs(t1 - t2) <= time_threshold); + + // 如果满足所有条件,则预测为碰撞 + if (valid_time) { + // 计算实际碰撞点(在交点之前) + double collision_distance = safe_distance; + double collision_time = + first_arrival_time - collision_distance / first_arrival_speed; + + if (collision_time < 0) { + collision_time = 0; + } + + // 根据碰撞时间计算两个物体的位置 + Vector2D pos1_at_collision = { pos1.x + vx1 * collision_time, + pos1.y + vy1 * collision_time }; + + Vector2D pos2_at_collision = { pos2.x + vx2 * collision_time, + pos2.y + vy2 * collision_time }; + + // 碰撞点在两个物体位置的中点 + result.collisionPoint = { + (pos1_at_collision.x + pos2_at_collision.x) / 2.0, + (pos1_at_collision.y + pos2_at_collision.y) / 2.0 }; + + Logger::debug("碰撞时间计算: first_arrival_time=", + first_arrival_time, ", safe_distance=", safe_distance, + ", collision_distance=", collision_distance, + ", collision_time=", collision_time); + + result.willCollide = true; + result.timeToCollision = collision_time; + result.timeToMinDistance = collision_time; + result.minDistance = safe_distance; + + // 更新物体状态 + result.object1State = + CollisionObjectState(pos1_at_collision, speed1, heading1); + result.object2State = + CollisionObjectState(pos2_at_collision, speed2, heading2); + + Logger::debug("交叉路径碰撞: collision_time=", collision_time, + ", collision1=(", pos1_at_collision.x, ",", + pos1_at_collision.y, ")", ", collision2=(", + pos2_at_collision.x, ",", pos2_at_collision.y, ")", + ", collision_point=(", result.collisionPoint.x, ",", + result.collisionPoint.y, ")"); + + return result; + } + } else { + result.type = CollisionType::PARALLEL; + // 平行运动判断: + // 1. 航向差很小(小于30度) + // 2. 横向距离大于安全距离 + // 3. 相对速度很小(说明速度接近) + if (angle_diff < 30.0) { + // 计算垂直于运动方向的向距离 + // 航向角转换为学坐标系中的旋转角度(逆时针为正) + double rotation_angle = (90.0 - heading1) * M_PI / 180.0; + + // 使用标准的二维坐标旋转式 + double dx_rotated = + dx * std::cos(rotation_angle) - dy * std::sin(rotation_angle); + double dy_rotated = + dx * std::sin(rotation_angle) + dy * std::cos(rotation_angle); + + // 横向距离就是旋转后的y坐标的绝对值 + double lateral_distance = std::abs(dy_rotated); + + Logger::debug("平行运动检测: angle_diff=", angle_diff, + ", heading1=", heading1, + ", rotation_angle=", rotation_angle * 180.0 / M_PI, + ", dx=", dx, ", dy=", dy, ", dx_rotated=", dx_rotated, + ", dy_rotated=", dy_rotated, + ", lateral_distance=", lateral_distance, + ", safe_distance=", safe_distance, + ", rel_speed=", rel_speed); + + // 如果横向距离大于等于安全距离,且相对速度很小,则不会碰撞 + if (lateral_distance >= safe_distance && rel_speed < 1.0) { + result.willCollide = false; + return result; + } + } + } + + // 5. 在时间窗口内预测碰撞 + const int STEPS = 120; // 增加采样点数以提高精度 + double dt = timeWindow / STEPS; // 时间步长 + + // 新计算速度分量(修正计算方式) + double vx1_sample = + speed1 * std::cos((90.0 - heading1) * M_PI / 180.0); // x 方向 + double vy1_sample = + speed1 * std::sin((90.0 - heading1) * M_PI / 180.0); // y 方向 + double vx2_sample = speed2 * std::cos((90.0 - heading2) * M_PI / 180.0); + double vy2_sample = speed2 * std::sin((90.0 - heading2) * M_PI / 180.0); + + // 如果当前已经碰撞 + if (current_distance <= safe_distance) { + result.willCollide = true; + result.timeToCollision = 0.0; + result.minDistance = current_distance; + result.timeToMinDistance = 0.0; + result.collisionPoint = { (pos1.x + pos2.x) / 2.0, + (pos1.y + pos2.y) / 2.0 }; + result.object1State = CollisionObjectState(pos1, speed1, heading1); + result.object2State = CollisionObjectState(pos2, speed2, heading2); + return result; + } + + // 在时间窗口内采样检查 + double prev_distance = current_distance; + Logger::debug("开始时间窗口采样: STEPS=", STEPS, ", dt=", dt, + ", timeWindow=", timeWindow, + ", safe_distance=", safe_distance, + ", current_distance=", current_distance); + for (int i = 1; i <= STEPS; ++i) { + double t = i * dt; + + // 计算t时的位置(使用修正后的速度分量) + Vector2D future1 = { pos1.x + vx1_sample * t, pos1.y + vy1_sample * t }; + + Vector2D future2 = { pos2.x + vx2_sample * t, pos2.y + vy2_sample * t }; + + // 计算t时的距离 + double dx_t = future2.x - future1.x; + double dy_t = future2.y - future1.y; + double distance = + std::sqrt(dx_t * dx_t + dy_t * dy_t); // 统一使用实际距离 + + // 更新小距离 + if (distance < result.minDistance) { + result.minDistance = distance; + result.timeToMinDistance = t; + // Logger::debug( + // "更新最小距离: min_distance=", distance, + // ", time_to_min=", t + // ); + } + + // 检查是否会碰撞 + if (distance <= safe_distance) { + result.willCollide = true; + // 使用当前距离和安全距离做插值,提高精确度 + double progress = + (distance - safe_distance) / (prev_distance - safe_distance); + double t_interp = t - dt * progress; + result.timeToCollision = t_interp; + + Logger::debug("找到碰撞: t=", t, ", distance=", distance, + ", prev_distance=", prev_distance, ", dt=", dt, + ", t_interp=", t_interp, ", future1=(", future1.x, + ",", future1.y, ")", ", future2=(", future2.x, ",", + future2.y, ")"); + + // 使用插值时间计算更精确的碰撞点 + Vector2D interp1 = { pos1.x + vx1_sample * t_interp, + pos1.y + vy1_sample * t_interp }; + Vector2D interp2 = { pos2.x + vx2_sample * t_interp, + pos2.y + vy2_sample * t_interp }; + result.collisionPoint = { (interp1.x + interp2.x) / 2.0, + (interp1.y + interp2.y) / 2.0 }; + result.object1State = + CollisionObjectState(interp1, speed1, heading1); + result.object2State = + CollisionObjectState(interp2, speed2, heading2); + break; + } + + prev_distance = distance; + + // 如果相对速度很小,且距离增加,可以提前退出 + if (rel_speed < 0.1 && i > 1) { + if (distance > prev_distance) { + break; + } + } + } + + return result; +} \ No newline at end of file diff --git a/src/detector/CollisionDetector.h b/src/detector/CollisionDetector.h new file mode 100644 index 0000000..27c3e7b --- /dev/null +++ b/src/detector/CollisionDetector.h @@ -0,0 +1,153 @@ +#ifndef AIRPORT_DETECTOR_COLLISION_DETECTOR_H +#define AIRPORT_DETECTOR_COLLISION_DETECTOR_H + +#include "types/BasicTypes.h" +#include "spatial/QuadTree.h" +#include "config/AirportBounds.h" +#include "vehicle/ControllableVehicles.h" +#include "config/SystemConfig.h" +#include "CollisionTypes.h" +#include "utils/Logger.h" +#include +#include + +// 碰撞风险等级 +enum class RiskLevel { + NONE = 0, // 无风险 + WARNING = 1, // 预警:进入预警区域 + CRITICAL = 2 // 告警:进入危险区域 +}; + +// 预警区域类型 +enum class WarningZoneType { + NONE = 0, // 无风险区域 + WARNING = 1, // 预警区域 + DANGER = 2 // 危险区域 +}; + +// 碰撞风险信息 +struct CollisionRisk { + std::string id1, id2; // 碰撞物体的ID + RiskLevel level; // 风险等级 + double distance; // 当前距离 + double minDistance; // 预测的最小距离(两个中心点之间的距离) + double relativeSpeed; // 相对速度 + Vector2D relativeMotion; // 相对运动向量 + WarningZoneType zoneType; // 预警区域类型 + double timeToCollision; // 预计碰撞时间(秒) + Vector2D collisionPoint; // 预计碰撞点 +}; + +// 冲突记录信息 +struct CollisionRecord { + Vector2D collisionPoint; // 冲突点位置 + int64_t detectedTime; // 检测到冲突的时间 + RiskLevel maxLevel; // 历史最高风险等级 + bool resolved; // 是否已解除 +}; + +// 冲突对象对的哈希函数 +struct CollisionPairHash { + std::size_t operator()(const std::pair& p) const { + return std::hash()(p.first + p.second); + } +}; + +class CollisionDetector { +public: + CollisionDetector(const AirportBounds& bounds, const ControllableVehicles& controllableVehicles); + + // 更新交通数据 + void updateTraffic(const std::vector& aircraft, + const std::vector& vehicles); + + // 检测所有可能的碰撞 + std::vector detectCollisions(); + + // 统一的碰撞检测函数 + CollisionResult checkCollision( + const MovingObject& obj1, + const MovingObject& obj2, + double timeWindow = 30.0) const; + +private: + const AirportBounds& airportBounds_; + QuadTree vehicleTree_; + std::vector aircraftData_; + const ControllableVehicles* controllableVehicles_; + + // 冲突记录映射表:<(id1,id2), CollisionRecord> + std::unordered_map, CollisionRecord, CollisionPairHash> collisionRecords_; + + // 将角度标准化到[0, 360)范围 + static double normalizeAngle(double angle) { + while (angle >= 360.0) angle -= 360.0; + while (angle < 0.0) angle += 360.0; + return angle; + } + + // 计算两个角度之间的最小差值(范围[0, 180]) + static double getAngleDifference(double angle1, double angle2) { + angle1 = normalizeAngle(angle1); + angle2 = normalizeAngle(angle2); + double diff = std::abs(angle1 - angle2); + return std::min(diff, 360.0 - diff); + } + + // 根据区域获取碰撞检测参数 + AreaConfig getCollisionParams(const Vector2D& position) const { + auto areaType = airportBounds_.getAreaType(position); + return airportBounds_.getAreaConfig(areaType); + } + + // 统一的风险评估函数 + std::pair evaluateRisk( + const CollisionResult& collisionResult, + const Vector2D& position, + MovingObjectType type1, + MovingObjectType type2) const; + + // 检查是否满足冲突解除条件 + bool checkCollisionResolved(const MovingObject& obj1, + const MovingObject& obj2, + const CollisionRecord& record, + double safeDistance) const; + + // 获取冲突记录的键 + std::pair getCollisionKey(const std::string& id1, + const std::string& id2) const { + return id1 < id2 ? std::make_pair(id1, id2) : std::make_pair(id2, id1); + } + + // 计算相对运动 + struct MovementVector { + double vx, vy; + + MovementVector(double speed, double heading) { + // 标准方位角:正北为0度,顺时针增加 + double radians = heading * M_PI / 180.0; + + // 计算速度分量 + vx = speed * std::sin(radians); // 东向分量 + vy = speed * std::cos(radians); // 北向分量 + } + }; + + // 预测位置的辅助函数 + Vector2D predictPosition(const Vector2D& position, double speed, double heading, double t) const { + double vx = speed * std::sin(heading * M_PI / 180.0); + double vy = speed * std::cos(heading * M_PI / 180.0); + return { + position.x + vx * t, + position.y + vy * t + }; + } + + // 基于圆形的碰撞检测核心函数 + CollisionResult predictCircleBasedCollision( + const Vector2D& pos1, double radius1, double speed1, double heading1, + const Vector2D& pos2, double radius2, double speed2, double heading2, + double timeWindow) const; +}; + +#endif // AIRPORT_DETECTOR_COLLISION_DETECTOR_H \ No newline at end of file diff --git a/src/detector/CollisionTypes.h b/src/detector/CollisionTypes.h new file mode 100644 index 0000000..30ce7dd --- /dev/null +++ b/src/detector/CollisionTypes.h @@ -0,0 +1,47 @@ +#pragma once + +#include "types/BasicTypes.h" +#include +#include + +// 碰撞类型 +enum class CollisionType { + HEAD_ON, // 相向碰撞 + CROSSING, // 交叉碰撞 + STATIC, // 静态碰撞 + PARALLEL // 平行运动 +}; + +// 碰撞时刻的物体状态 +struct CollisionObjectState { + Vector2D position; // 位置 + double speed; // 速度 + double heading; // 航向 + + CollisionObjectState() : speed(0.0), heading(0.0) {} + CollisionObjectState(const Vector2D& pos, double spd, double hdg) + : position(pos), speed(spd), heading(hdg) {} +}; + +// 碰撞预测结果 +struct CollisionResult { + bool willCollide; // 是否会碰撞 + double timeToCollision; // 碰撞时间(从当前时刻开始的秒数) + Vector2D collisionPoint; // 碰撞点位置 + double minDistance; // 最小距离 + double distance; // 当前距离 + double timeToMinDistance; // 到达最小距离的时间(从当前时刻开始的秒数) + CollisionType type; // 碰撞类型 + CollisionObjectState object1State; // 物体1在碰撞时刻的状态 + CollisionObjectState object2State; // 物体2在碰撞时刻的状态 + + // 构造函数 + CollisionResult() : + willCollide(false), + timeToCollision(std::numeric_limits::infinity()), + collisionPoint({0, 0}), + minDistance(std::numeric_limits::infinity()), + distance(std::numeric_limits::infinity()), + timeToMinDistance(std::numeric_limits::infinity()), + type(CollisionType::STATIC) {} +}; \ No newline at end of file diff --git a/src/detector/RectangleCollision.h b/src/detector/RectangleCollision.h new file mode 100644 index 0000000..3bf47b7 --- /dev/null +++ b/src/detector/RectangleCollision.h @@ -0,0 +1,117 @@ +#ifndef AIRPORT_DETECTOR_RECTANGLE_COLLISION_H +#define AIRPORT_DETECTOR_RECTANGLE_COLLISION_H + +#include "types/BasicTypes.h" +#include +#include +#include + +// 碰撞检测结果 +struct RectangleCollisionResult { + bool willCollide = false; // 是否会发生碰撞 + double timeToCollision = 0.0; // 到碰撞的时间 + Vector2D collisionPoint = {0, 0}; // 碰撞点 + double minDistance = 0.0; // 最小距离 +}; + +// 正方形表示 +struct Rectangle { + Vector2D center; // 中心点 + double size; // 边长 + Vector2D velocity; // 速度向量 + double heading; // 朝向(角度) + + // 构造函数 + Rectangle(const Vector2D& c, double s, const Vector2D& v, double h = 0.0) + : center(c), size(s), velocity(v), heading(h) { + } + + // 默认构造函数 + Rectangle() : center({0, 0}), size(0), velocity({0, 0}), heading(0) {} + + // 获取正方形的AABB(轴对齐包围盒) + // 正方形不需要考虑朝向,因为旋转后的正方形AABB保持不变 + void getAABB(double& x_min, double& x_max, double& y_min, double& y_max) const { + double half_size = size / 2.0; + x_min = center.x - half_size; + x_max = center.x + half_size; + y_min = center.y - half_size; + y_max = center.y + half_size; + } + + // 预测t时刻的位置 + Rectangle predictPosition(double t) const { + Rectangle future = *this; + future.center.x += velocity.x * t; + future.center.y += velocity.y * t; + return future; + } +}; + +// 预测两个矩形在给定时间窗口内是否会发生碰撞 +inline bool predictRectangleBasedCollision(const Rectangle& rect1, const Rectangle& rect2, double timeWindow) { + // 1. 首先检查当前是否碰撞 + double x1_min, x1_max, y1_min, y1_max; + double x2_min, x2_max, y2_min, y2_max; + + rect1.getAABB(x1_min, x1_max, y1_min, y1_max); + rect2.getAABB(x2_min, x2_max, y2_min, y2_max); + + // 检查当前是否重叠 + if (x1_max >= x2_min && x2_max >= x1_min && + y1_max >= y2_min && y2_max >= y1_min) { + return true; + } + + // 2. 检查未来一段时间内是否会碰撞 + // 计算相对速度 + double rel_vx = rect1.velocity.x - rect2.velocity.x; + double rel_vy = rect1.velocity.y - rect2.velocity.y; + double rel_speed = std::sqrt(rel_vx*rel_vx + rel_vy*rel_vy); + + // 如果相对速度接近0,使用当前距离判断 + if (rel_speed < 0.1) { + double dx = rect1.center.x - rect2.center.x; + double dy = rect1.center.y - rect2.center.y; + double distance = std::sqrt(dx*dx + dy*dy); + double safe_distance = rect1.size/2.0 + rect2.size/2.0; // 两个正方形从中心到边缘的距离之和 + + if (distance <= safe_distance) { + return true; + } + return false; + } + + // 3. 在时间窗口内采样检查 + const int STEPS = 60; + double dt = timeWindow / STEPS; + + for (int i = 1; i <= STEPS; ++i) { + double t = i * dt; + Rectangle future1 = rect1.predictPosition(t); + Rectangle future2 = rect2.predictPosition(t); + + future1.getAABB(x1_min, x1_max, y1_min, y1_max); + future2.getAABB(x2_min, x2_max, y2_min, y2_max); + + // 检查是否重叠 + if (x1_max >= x2_min && x2_max >= x1_min && + y1_max >= y2_min && y2_max >= y1_min) { + return true; + } + + // 计算中心点距离 + double dx = (future1.center.x - future2.center.x); + double dy = (future1.center.y - future2.center.y); + double distance = std::sqrt(dx*dx + dy*dy); + double safe_distance = rect1.size/2.0 + rect2.size/2.0; // 两个正方形从中心到边缘的距离之和 + + if (distance <= safe_distance) { + return true; + } + } + + return false; +} + +#endif // AIRPORT_DETECTOR_RECTANGLE_COLLISION_H \ No newline at end of file diff --git a/src/detector/SafetyZone.cpp b/src/detector/SafetyZone.cpp new file mode 100644 index 0000000..620172c --- /dev/null +++ b/src/detector/SafetyZone.cpp @@ -0,0 +1,126 @@ +#include "SafetyZone.h" +#include "utils/Logger.h" +#include "config/SystemConfig.h" +#include + +SafetyZone::SafetyZone(const Vector2D& center, double aircraftRadius, double specialVehicleRadius) + : center_(center) + , aircraftRadius_(aircraftRadius) + , specialVehicleRadius_(specialVehicleRadius) + , currentRadius_(0.0) + , state_(SafetyZoneState::INACTIVE) + , type_(SafetyZoneType::NONE) { + + Logger::debug("创建安全区: center=(", center_.x, ",", center_.y, + "), aircraftRadius=", aircraftRadius_, + ", specialVehicleRadius=", specialVehicleRadius_); +} + +bool SafetyZone::isInZone(const Vector2D& point) const { + // 如果未设置类型,直接返回 false + if (type_ == SafetyZoneType::NONE) { + return false; + } + + // 计算点到中心的距离 + double dx = point.x - center_.x; + double dy = point.y - center_.y; + double distance = std::sqrt(dx * dx + dy * dy); + + // 使用当前设置的安全区半径 + return distance <= currentRadius_; +} + +void SafetyZone::resetType() { + type_ = SafetyZoneType::NONE; + currentRadius_ = 0.0; + Logger::debug("重置安全区类型为 NONE"); +} + +double SafetyZone::getObjectRadius(const MovingObject& object) const { + const auto& config = SystemConfig::instance().collision_detection.prediction; + if (object.isAircraft()) { + double radius = config.aircraft_size / 2.0; + Logger::debug("目标 ", object.id, " 是飞机,尺寸半径: ", radius); + return radius; + } else if (object.isSpecialVehicle()) { + double radius = config.vehicle_size / 2.0; + Logger::debug("目标 ", object.id, " 是特勤车,尺寸半径: ", radius); + return radius; + } + return 0.0; +} + +bool SafetyZone::isTypeMatched(const MovingObject& object) const { + // 如果安全区类型未设置,返回 true + if (type_ == SafetyZoneType::NONE) { + return true; + } + + // 如果是无人车,返回 true + if (object.isUnmannedVehicle()) { + return true; + } + + // 如果安全区类型已设置,检查是否匹配 + if ((type_ == SafetyZoneType::AIRCRAFT && !object.isAircraft()) || + (type_ == SafetyZoneType::VEHICLE && !object.isSpecialVehicle())) { + Logger::debug("目标 ", object.id, " 类型与安全区类型不匹配,当前安全区类型: ", + type_ == SafetyZoneType::AIRCRAFT ? "飞机" : "特勤车"); + return false; + } + + return true; +} + +bool SafetyZone::isObjectInZone(const MovingObject& object) const { + // 计算点到中心的距离 + double dx = object.position.x - center_.x; + double dy = object.position.y - center_.y; + double distance = std::sqrt(dx * dx + dy * dy); + + // 如果安全区类型未设置,使用对应的临时半径进行检查 + double checkRadius = (type_ == SafetyZoneType::NONE) + ? (object.isAircraft() ? aircraftRadius_ : specialVehicleRadius_) + : currentRadius_; + + Logger::debug("检查目标 ", object.id, " 是否在安全区内: ", + "dx=", dx, ", dy=", dy, ", distance=", distance, + ", checkRadius=", checkRadius, + ", type=", type_ == SafetyZoneType::NONE ? "NONE" : + (type_ == SafetyZoneType::AIRCRAFT ? "AIRCRAFT" : "VEHICLE"), + ", center=(", center_.x, ",", center_.y, ")", + ", object=(", object.position.x, ",", object.position.y, ")"); + + return distance <= checkRadius; +} + +bool SafetyZone::tryActivate(const MovingObject& object) { + // 如果是无人车,不激活安全区 + if (object.isUnmannedVehicle()) { + Logger::debug("目标 ", object.id, " 是无人车,不激活安全区"); + return false; + } + + // 如果类型已设置且不匹配,不激活 + if (!isTypeMatched(object)) { + return false; + } + + // 检查是否在区内 + bool inZone = isObjectInZone(object); + if (!inZone) { + return false; + } + + // 如果安全区类型未设置,设置新的类型和半径 + if (type_ == SafetyZoneType::NONE) { + type_ = object.isAircraft() ? SafetyZoneType::AIRCRAFT : SafetyZoneType::VEHICLE; + currentRadius_ = object.isAircraft() ? aircraftRadius_ : specialVehicleRadius_; + Logger::debug("安全区设置为", object.isAircraft() ? "飞机" : "特勤车", "类型,半径: ", currentRadius_); + } + + // 激活安全区 + state_ = SafetyZoneState::ACTIVE; + return true; +} \ No newline at end of file diff --git a/src/detector/SafetyZone.h b/src/detector/SafetyZone.h new file mode 100644 index 0000000..5a96803 --- /dev/null +++ b/src/detector/SafetyZone.h @@ -0,0 +1,66 @@ +#pragma once + +#include "types/BasicTypes.h" + +// 安全区状态 +enum class SafetyZoneState { + INACTIVE, // 未激活 + ACTIVE // 已激活 +}; + +// 安全区类型 +enum class SafetyZoneType { + NONE, // 未设置 + AIRCRAFT, // 飞机安全区 + VEHICLE // 特勤车安全区 +}; + +// 安全区定义 +class SafetyZone { +public: + // 构造函数 + SafetyZone(const Vector2D& center, double aircraftSize, double specialVehicleSize); + + // 获取状态 + SafetyZoneState getState() const { return state_; } + SafetyZoneType getType() const { return type_; } + + // 设置状态 + void setState(SafetyZoneState state) { state_ = state; } + + // 重置类型为 NONE + void resetType(); + + // 检查点是否在安全区内 + bool isInZone(const Vector2D& point) const; + + // 检查目标是否在安全区内 - 纯检测函数,不修改状态 + bool isObjectInZone(const MovingObject& object) const; + + // 尝试更新安全区状态 - 处理状态更新 + bool tryActivate(const MovingObject& object); + + // 获取中心点 + const Vector2D& getCenter() const { return center_; } + + // 获取安全区尺寸配置 + double getAircraftRadius() const { return aircraftRadius_; } + double getSpecialVehicleRadius() const { return specialVehicleRadius_; } + + // 获取当前安全区半径 + double getCurrentRadius() const { return currentRadius_; } + +private: + Vector2D center_; // 安全区中心点 + double aircraftRadius_; // 飞机安全区半径 + double specialVehicleRadius_; // 特勤车安全区半径 + double currentRadius_; // 当前安全区半径 + SafetyZoneState state_; // 当前状态 + SafetyZoneType type_; // 当前类型 + + // 检查物体类型是否匹配当前安全区类型 + bool isTypeMatched(const MovingObject& object) const; + + // 获取物体的有效半径 + double getObjectRadius(const MovingObject& object) const; +}; \ No newline at end of file diff --git a/src/detector/TrafficLightDetector.cpp b/src/detector/TrafficLightDetector.cpp new file mode 100644 index 0000000..84e4f5a --- /dev/null +++ b/src/detector/TrafficLightDetector.cpp @@ -0,0 +1,72 @@ +#include "detector/TrafficLightDetector.h" +#include "utils/Logger.h" +#include "types/BasicTypes.h" +#include "core/System.h" +#include + +TrafficLightDetector::TrafficLightDetector(const IntersectionConfig& intersectionConfig, + ControllableVehicles& controllableVehicles, + System& system) + : intersection_config_(intersectionConfig) + , controllable_vehicles_(controllableVehicles) + , system_(system) {} + +void TrafficLightDetector::processSignal(const TrafficLightSignal& signal, + const std::vector& vehicles) { + // 根据红绿灯ID查找对应的路口 + const Intersection* intersection = intersection_config_.findByTrafficLightId(signal.trafficLightId); + if (!intersection) { + Logger::warning("未找到红绿灯对应的路口: ", signal.trafficLightId); + return; + } + + // 保存当前处理的信号 + current_signal_ = signal; + + // 检查每个车辆 + for (const auto& vehicle : vehicles) { + if (controllable_vehicles_.isControllable(vehicle.vehicleNo)) { + // 根据信号灯状态发送指令 + // FIXME: 临时修改 - 仅根据南北向状态发送指令,未考虑车辆方向和东西向状态。 + // 需要根据车辆行驶方向判断应该参考 ns_status 还是 ew_status。 + switch (signal.ns_status) { + case SignalStatus::RED: + sendSignalCommand(vehicle.vehicleNo, SignalState::RED); + break; + case SignalStatus::GREEN: + sendSignalCommand(vehicle.vehicleNo, SignalState::GREEN); + break; + case SignalStatus::YELLOW: + sendSignalCommand(vehicle.vehicleNo, SignalState::YELLOW); + break; + default: + Logger::warning("未知的信号灯状态"); + break; + } + } + } +} + +void TrafficLightDetector::sendSignalCommand(const std::string& vehicleNo, SignalState state) { + VehicleCommand cmd; + cmd.vehicleId = vehicleNo; + cmd.type = CommandType::SIGNAL; + cmd.reason = CommandReason::TRAFFIC_LIGHT; + cmd.signalState = state; + cmd.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + // 添加路口信息 + const Intersection* intersection = intersection_config_.findByTrafficLightId(current_signal_.trafficLightId); + if (intersection) { + cmd.intersectionId = intersection->id; + cmd.latitude = intersection->position.latitude; + cmd.longitude = intersection->position.longitude; + } + + controllable_vehicles_.sendCommand(vehicleNo, cmd); + system_.broadcastVehicleCommand(cmd); + Logger::debug("发送信号灯指令到车辆: ", vehicleNo, + " 路口: ", cmd.intersectionId, + " 状态: ", state == SignalState::RED ? "红灯" : state == SignalState::GREEN ? "绿灯" : "黄灯"); +} \ No newline at end of file diff --git a/src/detector/TrafficLightDetector.h b/src/detector/TrafficLightDetector.h new file mode 100644 index 0000000..d90124a --- /dev/null +++ b/src/detector/TrafficLightDetector.h @@ -0,0 +1,27 @@ +#pragma once + +#include "config/IntersectionConfig.h" +#include "vehicle/ControllableVehicles.h" +#include "types/TrafficLightTypes.h" +#include + +class System; + +class TrafficLightDetector { +public: + TrafficLightDetector(const IntersectionConfig& intersectionConfig, + ControllableVehicles& controllableVehicles, + System& system); + + void processSignal(const TrafficLightSignal& signal, + const std::vector& vehicles); + +private: + const IntersectionConfig& intersection_config_; + ControllableVehicles& controllable_vehicles_; + System& system_; + TrafficLightSignal current_signal_; + + // 发送信号灯指令 + void sendSignalCommand(const std::string& vehicleNo, SignalState state); +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c463c39 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,67 @@ +#include "core/System.h" +#include "utils/Logger.h" +#include "config/SystemConfig.h" +#include +#include +#include +#include + +volatile sig_atomic_t running = true; + +void signalHandler(int signum) { + std::cout << "Interrupt signal (" << signum << ") received.\n"; + running = false; +} + +LogLevel getLogLevelFromString(const std::string& level) { + if (level == "debug") return LogLevel::DEBUG; + if (level == "info") return LogLevel::INFO; + if (level == "warning") return LogLevel::WARNING; + if (level == "error") return LogLevel::ERROR; + return LogLevel::INFO; // 默认级别 +} + +int main(int argc, char* argv[]) { + try { + // 设置信号处理 + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + + // 创建日志目录 + std::filesystem::create_directories("logs"); + + // 加载系统配置 + SystemConfig::instance().load("config/system_config.json"); + + // 初始化日志系统 + Logger::initialize(SystemConfig::instance().logging.file, getLogLevelFromString(SystemConfig::instance().logging.level)); + Logger::info("Starting system..."); + Logger::debug("Log level set to: ", SystemConfig::instance().logging.level); + + // 初始化系统 + System system; + if (!system.initialize()) { + Logger::error("Failed to initialize system"); + return 1; + } + + // 启动系统 + system.start(); + Logger::info("System running..."); + + // 主循环 + while (running) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + // 停止系统 + system.stop(); + Logger::info("System stopped"); + + return 0; + } + catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << std::endl; + return 1; + } +} \ No newline at end of file diff --git a/src/mock/MockDataService.cpp b/src/mock/MockDataService.cpp new file mode 100644 index 0000000..fe31d76 --- /dev/null +++ b/src/mock/MockDataService.cpp @@ -0,0 +1,168 @@ +#include "mock/MockDataService.h" +#include +#include +#include + +MockDataService::MockDataService(const AirportBounds& bounds) + : bounds_(bounds) +{ +} + +std::vector MockDataService::generateAircraftData() { + std::vector aircraft; + + // 生成跑道上的航空器 + for (int i = 0; i < 5; ++i) { + Aircraft a; + a.flightNo = generateAircraftId(); + a.position = generatePosition(AreaType::RUNWAY); + a.speed = generateSpeed(AreaType::RUNWAY); + a.heading = generateHeading(AreaType::RUNWAY); + a.altitude = generateAltitude(AreaType::RUNWAY); + aircraft.push_back(a); + } + + // 生成滑行道上的航空器 + for (int i = 0; i < 10; ++i) { + Aircraft a; + a.flightNo = generateAircraftId(); + a.position = generatePosition(AreaType::TAXIWAY); + a.speed = generateSpeed(AreaType::TAXIWAY); + a.heading = generateHeading(AreaType::TAXIWAY); + a.altitude = generateAltitude(AreaType::TAXIWAY); + aircraft.push_back(a); + } + + // 生成停机位的航空器 + for (int i = 0; i < 15; ++i) { + Aircraft a; + a.flightNo = generateAircraftId(); + a.position = generatePosition(AreaType::GATE); + a.speed = generateSpeed(AreaType::GATE); + a.heading = generateHeading(AreaType::GATE); + a.altitude = generateAltitude(AreaType::GATE); + aircraft.push_back(a); + } + + return aircraft; +} + +std::vector MockDataService::generateVehicleData() { + std::vector vehicles; + + // 生成服务区的车辆 + for (int i = 0; i < 20; ++i) { + Vehicle v; + v.vehicleNo = generateVehicleId(); + v.position = generatePosition(AreaType::SERVICE); + v.speed = generateSpeed(AreaType::SERVICE); + v.heading = generateHeading(AreaType::SERVICE); + vehicles.push_back(v); + } + + // 生成停机位的车辆 + for (int i = 0; i < 15; ++i) { + Vehicle v; + v.vehicleNo = generateVehicleId(); + v.position = generatePosition(AreaType::GATE); + v.speed = generateSpeed(AreaType::GATE); + v.heading = generateHeading(AreaType::GATE); + vehicles.push_back(v); + } + + return vehicles; +} + +Vector2D MockDataService::generatePosition(AreaType areaType) { + const auto& config = bounds_.getAreaConfig(areaType); + const auto& areaBounds = bounds_.getAreaBounds(areaType); + + std::uniform_real_distribution x_dist(areaBounds.x, + areaBounds.x + areaBounds.width); + std::uniform_real_distribution y_dist(areaBounds.y, + areaBounds.y + areaBounds.height); + + return {x_dist(rng_), y_dist(rng_)}; +} + +double MockDataService::generateSpeed(AreaType areaType) { + // 根据区域类型返回合适的速度范围 + switch (areaType) { + case AreaType::RUNWAY: + return std::uniform_real_distribution{30.0, 80.0}(rng_); + case AreaType::TAXIWAY: + return std::uniform_real_distribution{5.0, 15.0}(rng_); + case AreaType::GATE: + return std::uniform_real_distribution{0.0, 5.0}(rng_); + case AreaType::SERVICE: + return std::uniform_real_distribution{0.0, 10.0}(rng_); + default: + return 0.0; + } +} + +double MockDataService::generateHeading(AreaType areaType) { + // 根据区域类型返回合适的航向范围 + switch (areaType) { + case AreaType::RUNWAY: + // 跑道通常只有两个方向 + return std::uniform_int_distribution{0, 1}(rng_) * 180.0; + default: + // 其他区域可以是任意方向 + return std::uniform_real_distribution{0.0, 360.0}(rng_); + } +} + +double MockDataService::generateAltitude(AreaType areaType) { + // 根据区域类型返回合适的高度范围 + switch (areaType) { + case AreaType::RUNWAY: + return std::uniform_real_distribution{0.0, 15.0}(rng_); + case AreaType::TAXIWAY: + return std::uniform_real_distribution{0.0, 5.0}(rng_); + default: + return 0.0; + } +} + +std::string MockDataService::generateAircraftId() { + std::stringstream ss; + ss << "AC" << std::setw(4) << std::setfill('0') << ++aircraftCounter_; + return ss.str(); +} + +std::string MockDataService::generateVehicleId() { + std::stringstream ss; + ss << "VH" << std::setw(4) << std::setfill('0') << ++vehicleCounter_; + return ss.str(); +} + +std::vector MockDataService::generateTrafficLightData() { + std::vector signals; + + // 生成 5-10 个红绿灯信号 + std::uniform_int_distribution<> numSignals(5, 10); + int count = numSignals(rng_); + + for (int i = 0; i < count; ++i) { + TrafficLightSignal signal; + signal.trafficLightId = generateTrafficLightId(); + signal.status = generateTrafficLightState(); + signal.timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + signals.push_back(signal); + } + + return signals; +} + +std::string MockDataService::generateTrafficLightId() { + std::stringstream ss; + ss << "TL" << std::setw(4) << std::setfill('0') << ++trafficLightCounter_; + return ss.str(); +} + +SignalStatus MockDataService::generateTrafficLightState() { + std::uniform_int_distribution<> dist(0, 2); + int state = dist(rng_); + return static_cast(state); +} \ No newline at end of file diff --git a/src/mock/MockDataService.h b/src/mock/MockDataService.h new file mode 100644 index 0000000..2852115 --- /dev/null +++ b/src/mock/MockDataService.h @@ -0,0 +1,43 @@ +#ifndef AIRPORT_MOCK_MOCKDATASERVICE_H +#define AIRPORT_MOCK_MOCKDATASERVICE_H + +#include "types/BasicTypes.h" +#include "config/AirportBounds.h" +#include "types/TrafficLightTypes.h" +#include +#include + +class MockDataService { +public: + explicit MockDataService(const AirportBounds& bounds); + + std::vector generateAircraftData(); + std::vector generateVehicleData(); + std::vector generateTrafficLightData(); + +private: + const AirportBounds& bounds_; + std::mt19937 rng_{std::random_device{}()}; + + // 在指定区域内生成随机位置 + Vector2D generatePosition(AreaType areaType); + + // 根据区域生成合适的速度和航向 + double generateSpeed(AreaType areaType); + double generateHeading(AreaType areaType); + double generateAltitude(AreaType areaType); + + // 生成ID + std::string generateAircraftId(); + std::string generateVehicleId(); + std::string generateTrafficLightId(); + + // 生成红绿灯状态 + SignalStatus generateTrafficLightState(); + + int aircraftCounter_ = 0; + int vehicleCounter_ = 0; + int trafficLightCounter_ = 0; +}; + +#endif // AIRPORT_MOCK_MOCKDATASERVICE_H \ No newline at end of file diff --git a/src/network/HTTPClient.cpp b/src/network/HTTPClient.cpp new file mode 100644 index 0000000..a1408f0 --- /dev/null +++ b/src/network/HTTPClient.cpp @@ -0,0 +1,142 @@ +#include "HTTPClient.h" +#include "utils/Logger.h" +#include + +namespace { + std::string getSignalStateString(SignalState state) { + switch (state) { + case SignalState::RED: + return "RED"; + case SignalState::YELLOW: + return "YELLOW"; + case SignalState::GREEN: + return "GREEN"; + default: + return "UNKNOWN"; + } + } +} // anonymous namespace + +HTTPClient::HTTPClient() { + curl_global_init(CURL_GLOBAL_ALL); + curl_ = curl_easy_init(); + if (!curl_) { + throw std::runtime_error("Failed to initialize CURL"); + } +} + +HTTPClient::~HTTPClient() { + if (curl_) { + curl_easy_cleanup(curl_); + } + curl_global_cleanup(); +} + +size_t HTTPClient::WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t total_size = size * nmemb; + std::string* response = static_cast(userp); + response->append(static_cast(contents), total_size); + return total_size; +} + +bool HTTPClient::sendCommand(const std::string& ip, int port, const VehicleCommand& command) const { + if (!curl_) { + Logger::error("CURL not initialized"); + return false; + } + + // 构造请求URL + std::stringstream url; + url << "http://" << ip << ":" << port << "/api/VehicleCommandInfo"; + + // 生成消息唯一 id + std::stringstream transId; + transId << std::hex << std::chrono::system_clock::now().time_since_epoch().count(); + + // 构造请求体(必填字段) + nlohmann::json request = { + {"transId", transId.str()}, + {"timestamp", command.timestamp}, + {"vehicleID", command.vehicleId}, + {"commandType", [&]() { + switch (command.type) { + case CommandType::SIGNAL: return "SIGNAL"; + case CommandType::ALERT: return "ALERT"; + case CommandType::WARNING: return "WARNING"; + case CommandType::RESUME: return "RESUME"; + case CommandType::PARKING: return "PARKING"; + default: return "UNKNOWN"; + } + }()}, + {"commandReason", [&]() { + switch (command.reason) { + case CommandReason::TRAFFIC_LIGHT: return "TRAFFIC_LIGHT"; + case CommandReason::AIRCRAFT_CROSSING: return "AIRCRAFT_CROSSING"; + case CommandReason::SPECIAL_VEHICLE: return "SPECIAL_VEHICLE"; + case CommandReason::AIRCRAFT_PUSH: return "AIRCRAFT_PUSH"; + case CommandReason::RESUME_TRAFFIC: return "RESUME_TRAFFIC"; + case CommandReason::PARKING_SIDE: return "PARKING_SIDE"; + default: return "UNKNOWN"; + } + }()}, + {"latitude", command.latitude}, + {"longitude", command.longitude}, + {"signalState", getSignalStateString(command.signalState)}, + {"intersectionId", command.intersectionId}, + {"relativeSpeed", command.relativeSpeed}, + {"relativeMotionX", command.relativeMotionX}, + {"relativeMotionY", command.relativeMotionY}, + {"minDistance", command.minDistance} + }; + + std::string request_body = request.dump(); + response_buffer_.clear(); + + // 设置CURL选项 + curl_easy_setopt(curl_, CURLOPT_URL, url.str().c_str()); + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, request_body.c_str()); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response_buffer_); + + // 设置请求头 + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + + // 发送请求 + CURLcode res = curl_easy_perform(curl_); + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + Logger::error("Failed to send command: ", curl_easy_strerror(res)); + return false; + } + + // 检查响应状态码 + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code == 200) { + try { + // 解析响应 + nlohmann::json response = nlohmann::json::parse(response_buffer_); + int code = response["code"].get(); + std::string msg = response["msg"].get(); + + if (code == 200) { + Logger::debug("Command sent successfully: ", request_body); + return true; + } else { + Logger::error("Command failed with code: ", code, " message: ", msg); + return false; + } + } catch (const std::exception& e) { + Logger::error("Failed to parse response: ", e.what()); + return false; + } + } else { + Logger::error("Command failed with HTTP code: ", http_code, " response: ", response_buffer_); + return false; + } +} \ No newline at end of file diff --git a/src/network/HTTPClient.h b/src/network/HTTPClient.h new file mode 100644 index 0000000..998fa74 --- /dev/null +++ b/src/network/HTTPClient.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include "types/VehicleCommand.h" +#include "nlohmann/json.hpp" + +class HTTPClient { +public: + HTTPClient(); + ~HTTPClient(); + + // 发送控制指令 + bool sendCommand(const std::string& ip, int port, const VehicleCommand& command) const; + +private: + static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp); + CURL* curl_; + mutable std::string response_buffer_; +}; \ No newline at end of file diff --git a/src/network/HTTPDataSource.cpp b/src/network/HTTPDataSource.cpp new file mode 100644 index 0000000..0f16b47 --- /dev/null +++ b/src/network/HTTPDataSource.cpp @@ -0,0 +1,605 @@ +#include "HTTPDataSource.h" +#include "HTTPClient.h" +#include "utils/Logger.h" +#include +#include + +using json = nlohmann::json; + +size_t HTTPDataSource::WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t total_size = size * nmemb; + std::string* response = static_cast(userp); + response->append(static_cast(contents), total_size); + return total_size; +} + +size_t HTTPDataSource::DebugCallback(CURL* handle, curl_infotype type, char* data, size_t size, void* userp) { + return 0; +} + +HTTPDataSource::HTTPDataSource(const DataSourceConfig& config) + : config_(config) + , last_health_check_(std::chrono::steady_clock::now()) { + curl_ = curl_easy_init(); + if (!curl_) { + Logger::error("Failed to initialize CURL"); + } +} + +HTTPDataSource::~HTTPDataSource() { + disconnect(); + if (curl_) { + curl_easy_cleanup(curl_); + } +} + +bool HTTPDataSource::connect() { + if (!curl_) { + curl_ = curl_easy_init(); + if (!curl_) { + Logger::error("Failed to initialize CURL"); + return false; + } + } + return true; +} + +void HTTPDataSource::disconnect() { + if (curl_) { + curl_easy_cleanup(curl_); + curl_ = nullptr; + } +} + +bool HTTPDataSource::isAvailable() const { + return curl_ != nullptr; +} + +bool HTTPDataSource::tryReconnect() { + if (is_reconnecting_) { + return false; + } + + is_reconnecting_ = true; + + try { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - last_connect_attempt_).count(); + + if (elapsed < config_.position.timeout_ms) { + is_reconnecting_ = false; + return false; + } + + last_connect_attempt_ = now; + + for (int retry = 0; retry < MAX_RETRIES; ++retry) { + if (connect()) { + is_reconnecting_ = false; + return true; + } + + if (retry < MAX_RETRIES - 1) { + std::this_thread::sleep_for(std::chrono::milliseconds(config_.position.timeout_ms / 3)); + } + } + + Logger::error("Failed to reconnect after ", MAX_RETRIES, " attempts"); + is_reconnecting_ = false; + return false; + } catch (const std::exception& e) { + Logger::error("Error during reconnection: ", e.what()); + is_reconnecting_ = false; + return false; + } +} + +bool HTTPDataSource::checkConnectionHealth() { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - last_health_check_).count(); + + if (elapsed < HEALTH_CHECK_INTERVAL) { + return true; + } + + last_health_check_ = now; + return curl_ != nullptr; +} + +bool HTTPDataSource::ensureConnected() { + if (isAvailable() && checkConnectionHealth()) { + return true; + } + + return tryReconnect(); +} + +bool HTTPDataSource::sendRequest(const std::string& url, const AuthState* auth_state, + std::string& response, HttpMethod method, const std::string& body) { + if (!curl_) { + Logger::error("CURL not initialized"); + return false; + } + + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, config_.position.timeout_ms / 1000); + curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT, config_.position.timeout_ms / 1000); + curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1L); + + if (method == HttpMethod::GET) { + curl_easy_setopt(curl_, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl_, CURLOPT_POST, 0L); + } else if (method == HttpMethod::POST) { + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + curl_easy_setopt(curl_, CURLOPT_HTTPGET, 0L); + if (!body.empty()) { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, body.c_str()); + } else { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, ""); + } + } + + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Accept: application/json"); + headers = curl_slist_append(headers, "Cache-Control: no-cache"); + headers = curl_slist_append(headers, "Expect:"); + + if (method == HttpMethod::POST) { + headers = curl_slist_append(headers, "Content-Type: application/json"); + } + + if (auth_state && auth_state->is_authenticated && !auth_state->token.empty()) { + std::string auth_header = "Authorization: " + auth_state->token; + headers = curl_slist_append(headers, auth_header.c_str()); + } + + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers); + + response.clear(); + CURLcode res = curl_easy_perform(curl_); + curl_slist_free_all(headers); + + if (res != CURLE_OK) { + Logger::error("请求失败: ", curl_easy_strerror(res)); + return false; + } + + long http_code = 0; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code != 200) { + Logger::error("HTTP 请求失败,状态码: ", http_code); + return false; + } + + return true; +} + +bool HTTPDataSource::fetchPositionAircraftData(std::vector& aircraft) { + std::lock_guard lock(mutex_); + + if (!ensureConnected()) { + Logger::error("连接失败"); + return false; + } + + if (!ensureAuthenticated(config_.position.auth, + config_.position.host, + config_.position.port, + position_auth_, + DataSourceType::POSITION)) { + Logger::error("认证失败"); + return false; + } + + std::string url = "http://" + config_.position.host + ":" + + std::to_string(config_.position.port) + + config_.position.aircraft_path; + + std::string response; + if (!sendRequest(url, &position_auth_, response, HttpMethod::GET)) { + Logger::error("请求失败"); + return false; + } + + return parsePositionAircraftResponse(response, aircraft); +} + +bool HTTPDataSource::fetchPositionVehicleData(std::vector& vehicles) { + std::lock_guard lock(mutex_); + + if (!ensureConnected()) { + return false; + } + + if (!ensureAuthenticated(config_.position.auth, + config_.position.host, + config_.position.port, + position_auth_, + DataSourceType::POSITION)) { + return false; + } + + std::string url = "http://" + config_.position.host + ":" + + std::to_string(config_.position.port) + + config_.position.vehicle_path; + + std::string response; + if (!sendRequest(url, &position_auth_, response, HttpMethod::GET)) { + return false; + } + + return parsePositionVehicleResponse(response, vehicles); +} + +bool HTTPDataSource::fetchTrafficLightSignals(std::vector& signals) { + std::lock_guard lock(mutex_); + + if (!ensureConnected()) { + return false; + } + + if (!ensureAuthenticated(config_.traffic_light.auth, + config_.traffic_light.host, + config_.traffic_light.port, + traffic_light_auth_, + DataSourceType::TRAFFIC_LIGHT)) { + return false; + } + + std::string url = "http://" + config_.traffic_light.host + ":" + + std::to_string(config_.traffic_light.port) + + config_.traffic_light.signal_path; + + std::string response; + if (!sendRequest(url, &traffic_light_auth_, response, HttpMethod::GET)) { + return false; + } + + return parseTrafficLightResponse(response, signals); +} + +std::string getSignalStateString(SignalState state) { + switch (state) { + case SignalState::RED: + return "RED"; + case SignalState::YELLOW: + return "YELLOW"; + case SignalState::GREEN: + return "GREEN"; + default: + return "GREEN"; // 默认为绿灯 + } +} + +bool HTTPDataSource::sendUnmannedVehicleCommand(const std::string& vehicle_id, const VehicleCommand& command) { + std::lock_guard lock(mutex_); + + if (!ensureConnected()) { + return false; + } + + if (!ensureAuthenticated(config_.vehicle.auth, + config_.vehicle.host, + config_.vehicle.port, + unmanned_vehicle_auth_, + DataSourceType::UNMANNED)) { + return false; + } + + std::string url = "http://" + config_.vehicle.host + ":" + + std::to_string(config_.vehicle.port) + + config_.vehicle.command_path; + + std::stringstream transId; + transId << std::hex << std::chrono::system_clock::now().time_since_epoch().count(); + + nlohmann::json request = { + {"transId", transId.str()}, + {"timestamp", command.timestamp}, + {"vehicleID", command.vehicleId}, + {"commandType", [&]() { + switch (command.type) { + case CommandType::SIGNAL: return "SIGNAL"; + case CommandType::ALERT: return "ALERT"; + case CommandType::WARNING: return "WARNING"; + case CommandType::RESUME: return "RESUME"; + default: return "UNKNOWN"; + } + }()}, + {"commandReason", [&]() { + switch (command.reason) { + case CommandReason::TRAFFIC_LIGHT: return "TRAFFIC_LIGHT"; + case CommandReason::AIRCRAFT_CROSSING: return "AIRCRAFT_CROSSING"; + case CommandReason::SPECIAL_VEHICLE: return "SPECIAL_VEHICLE"; + case CommandReason::AIRCRAFT_PUSH: return "AIRCRAFT_PUSH"; + case CommandReason::RESUME_TRAFFIC: return "RESUME_TRAFFIC"; + default: return "UNKNOWN"; + } + }()}, + {"latitude", command.latitude}, + {"longitude", command.longitude}, + {"signalState", getSignalStateString(command.signalState)}, + {"intersectionId", command.intersectionId} + }; + + if (command.type == CommandType::ALERT || command.type == CommandType::WARNING) { + request["relativeSpeed"] = command.relativeSpeed; + request["relativeMotionX"] = command.relativeMotionX; + request["relativeMotionY"] = command.relativeMotionY; + request["minDistance"] = command.minDistance; + } + + std::string response; + if (!sendRequest(url, &unmanned_vehicle_auth_, response, HttpMethod::POST, request.dump())) { + return false; + } + + try { + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("code") || !j.contains("msg")) { + Logger::error("Invalid command response format"); + return false; + } + + if (j["code"].get() != 200) { + Logger::error("Command failed: ", j["msg"].get()); + return false; + } + + return true; + } catch (const std::exception& e) { + Logger::error("Failed to parse command response: ", e.what()); + return false; + } +} + +bool HTTPDataSource::fetchUnmannedVehicleStatus(const std::string& vehicle_id, std::string& status) { + std::lock_guard lock(mutex_); + + if (!ensureConnected()) { + return false; + } + + if (!ensureAuthenticated(config_.vehicle.auth, + config_.vehicle.host, + config_.vehicle.port, + unmanned_vehicle_auth_, + DataSourceType::UNMANNED)) { + return false; + } + + std::string url = "http://" + config_.vehicle.host + ":" + + std::to_string(config_.vehicle.port) + + config_.vehicle.status_path + "?vehicleId=" + vehicle_id; + + std::string response; + if (!sendRequest(url, &unmanned_vehicle_auth_, response, HttpMethod::GET)) { + return false; + } + + try { + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("status") || !j.contains("data")) { + Logger::error("Invalid vehicle status response format"); + return false; + } + + if (j["status"].get() != 200) { + Logger::error("Failed to get vehicle status"); + return false; + } + + status = j["data"].dump(); + return true; + } catch (const std::exception& e) { + Logger::error("Failed to parse vehicle status response: ", e.what()); + return false; + } +} + +bool HTTPDataSource::authenticatePosition(const AuthConfig& auth_config, + const std::string& host, int port, AuthState& auth_state) { + if (!auth_config.auth_required) { + auth_state.is_authenticated = true; + return true; + } + + std::lock_guard lock(auth_mutex_); + + if (!curl_) { + Logger::error("CURL not initialized"); + return false; + } + + std::string url = "http://" + host + ":" + std::to_string(port) + + auth_config.auth_path + "?username=" + auth_config.username + + "&password=" + auth_config.password; + + std::string response; + if (!sendRequest(url, nullptr, response, HttpMethod::POST, "")) { + Logger::error("认证请求失败"); + return false; + } + + try { + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("status") || !j.contains("msg") || !j.contains("data")) { + Logger::error("认证响应缺少必需字段"); + return false; + } + + if (j["status"].get() != 200) { + Logger::error("认证失败: ", j["msg"].get()); + return false; + } + + std::string token = j["data"].get(); + if (token.substr(0, 7) != "Bearer ") { + token = "Bearer " + token; + } + auth_state.token = token; + auth_state.is_authenticated = true; + auth_state.last_auth_time = std::chrono::steady_clock::now(); + + return true; + } catch (const std::exception& e) { + Logger::error("解析认证响应失败: ", e.what(), ", 响应内容: ", response); + return false; + } +} + +bool HTTPDataSource::authenticateUnmanned(const AuthConfig& auth_config, + const std::string& host, int port, AuthState& auth_state) { + auth_state.is_authenticated = true; + return true; +} + +bool HTTPDataSource::authenticateTrafficLight(const AuthConfig& auth_config, + const std::string& host, int port, AuthState& auth_state) { + auth_state.is_authenticated = true; + return true; +} + +bool HTTPDataSource::ensureAuthenticated(const AuthConfig& auth_config, const std::string& host, + int port, AuthState& auth_state, DataSourceType type) { + if (!auth_config.auth_required) { + return true; + } + + if (!auth_state.is_authenticated) { + switch (type) { + case DataSourceType::POSITION: + return authenticatePosition(auth_config, host, port, auth_state); + case DataSourceType::UNMANNED: + return authenticateUnmanned(auth_config, host, port, auth_state); + case DataSourceType::TRAFFIC_LIGHT: + return authenticateTrafficLight(auth_config, host, port, auth_state); + default: + return false; + } + } + + return true; +} + +bool HTTPDataSource::parsePositionAircraftResponse(const std::string& response, std::vector& aircraft) { + try { + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("status") || !j.contains("data")) { + Logger::error("Invalid aircraft response format"); + return false; + } + + if (j["status"].get() != 200) { + Logger::error("Failed to get aircraft data: ", j["msg"].get()); + return false; + } + + const auto& data = j["data"]; + aircraft.clear(); + for (const auto& item : data) { + Aircraft ac; + ac.flightNo = item["flightNo"].get(); + ac.id = ac.flightNo; + ac.geo.longitude = item["longitude"].get(); + ac.geo.latitude = item["latitude"].get(); + ac.timestamp = item["time"].get(); + + if (item.contains("altitude")) { + ac.altitude = item["altitude"].get(); + } + if (item.contains("trackNumber")) { + ac.trackNumber = item["trackNumber"].get(); + } + aircraft.push_back(ac); + } + return true; + } catch (const std::exception& e) { + Logger::error("Failed to parse aircraft response: ", e.what()); + return false; + } +} + +bool HTTPDataSource::parsePositionVehicleResponse(const std::string& response, std::vector& vehicles) { + try { + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("status") || !j.contains("data")) { + Logger::error("Invalid vehicle response format"); + return false; + } + + if (j["status"].get() != 200) { + Logger::error("Failed to get vehicle data: ", j["msg"].get()); + return false; + } + + const auto& data = j["data"]; + vehicles.clear(); + for (const auto& item : data) { + Vehicle vehicle; + vehicle.vehicleNo = item["vehicleNo"].get(); + vehicle.id = vehicle.vehicleNo; + vehicle.geo.longitude = item["longitude"].get(); + vehicle.geo.latitude = item["latitude"].get(); + vehicle.timestamp = item["time"].get(); + + if (item.contains("direction")) { + vehicle.heading = item["direction"].get(); + } + if (item.contains("speed")) { + vehicle.speed = item["speed"].get(); + } + vehicles.push_back(vehicle); + } + return true; + } catch (const std::exception& e) { + Logger::error("Failed to parse vehicle response: ", e.what()); + return false; + } +} + +bool HTTPDataSource::parseTrafficLightResponse(const std::string& response, std::vector& signals) { + try { + nlohmann::json j = nlohmann::json::parse(response); + if (!j.contains("status") || !j.contains("data")) { + Logger::error("Invalid traffic light response format"); + return false; + } + + if (j["status"].get() != 200) { + Logger::error("Failed to get traffic light data: ", j["msg"].get()); + return false; + } + + const auto& data = j["data"]; + signals.clear(); + for (const auto& item : data) { + TrafficLightSignal signal; + signal.trafficLightId = item["id"].get(); + signal.ns_status = [&]() { + int state = item["ns_status"].get(); + switch (state) { + case 0: return SignalStatus::RED; + case 1: return SignalStatus::GREEN; + case 2: return SignalStatus::YELLOW; + default: return SignalStatus::UNKNOWN; + } + }(); + signal.timestamp = item["timestamp"].get(); + signal.intersectionId = item["intersection"].get(); + signal.latitude = item["position"]["latitude"].get(); + signal.longitude = item["position"]["longitude"].get(); + signals.push_back(signal); + } + return true; + } catch (const std::exception& e) { + Logger::error("Failed to parse traffic light response: ", e.what()); + return false; + } +} diff --git a/src/network/HTTPDataSource.h b/src/network/HTTPDataSource.h new file mode 100644 index 0000000..25b42d3 --- /dev/null +++ b/src/network/HTTPDataSource.h @@ -0,0 +1,96 @@ +#ifndef AIRPORT_NETWORK_HTTP_DATA_SOURCE_H +#define AIRPORT_NETWORK_HTTP_DATA_SOURCE_H + +#include "collector/DataSource.h" +#include "spatial/CoordinateConverter.h" +#include +#include "collector/DataSourceConfig.h" +#include +#include +#include +#include "core/System.h" +#include +#include "types/VehicleCommand.h" + +class HTTPDataSource : public DataSource { +public: + enum class HttpMethod { + GET, + POST + }; + + explicit HTTPDataSource(const DataSourceConfig& config); + ~HTTPDataSource() override; + + const DataSourceConfig& getConfig() const { return config_; } + + DataSourceConfig config_; + bool connect() override; + void disconnect() override; + bool isAvailable() const override; + + // 位置数据接口 + bool fetchPositionAircraftData(std::vector& aircraft) override; + bool fetchPositionVehicleData(std::vector& vehicles) override; + + // 无人车接口 + bool sendUnmannedVehicleCommand(const std::string& vehicle_id, const VehicleCommand& command) override; + bool fetchUnmannedVehicleStatus(const std::string& vehicle_id, std::string& status) override; + + // 红绿灯接口 + bool fetchTrafficLightSignals(std::vector& signals) override; + +private: + std::mutex mutex_; + CURL* curl_; + + std::chrono::steady_clock::time_point last_connect_attempt_; + std::chrono::steady_clock::time_point last_health_check_; // 上次健康检查时间 + static constexpr int MAX_RETRIES = 3; // 单次连接最大重试次数 + static constexpr int HEALTH_CHECK_INTERVAL = 60; // 健康检查间隔(秒) + std::atomic is_reconnecting_{false}; // 重连状态标志 + + // 每个数据源的认证状态 + struct AuthState { + bool is_authenticated = false; + std::string token; + std::chrono::steady_clock::time_point last_auth_time; + }; + + AuthState position_auth_; // 位置数据源认证状态 + AuthState unmanned_vehicle_auth_; // 无人车数据源认证状态 + AuthState traffic_light_auth_; // 红绿灯数据源认证状态 + std::mutex auth_mutex_; // 认证相关的互斥锁 + + // 认证相关方法 + bool ensureAuthenticated(const AuthConfig& auth_config, const std::string& host, + int port, AuthState& auth_state, DataSourceType type); + + bool tryReconnect(); + bool ensureConnected(); + bool checkConnectionHealth(); // 检查连接健康状况 + + bool sendRequest(const std::string& url, const AuthState* auth_state, + std::string& response, HttpMethod method = HttpMethod::GET, + const std::string& body = ""); + + // 位置数据响应解析 + bool parsePositionAircraftResponse(const std::string& response, std::vector& aircraft); + bool parsePositionVehicleResponse(const std::string& response, std::vector& vehicles); + + // 红绿灯响应解析 + bool parseTrafficLightResponse(const std::string& response, std::vector& signals); + + static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp); + static size_t DebugCallback(CURL* handle, curl_infotype type, char* data, size_t size, void* userp); + + // 不同数据源的认证方法 + bool authenticatePosition(const AuthConfig& auth_config, const std::string& host, + int port, AuthState& auth_state); + bool authenticateUnmanned(const AuthConfig& auth_config, const std::string& host, + int port, AuthState& auth_state); + bool authenticateTrafficLight(const AuthConfig& auth_config, const std::string& host, + int port, AuthState& auth_state); +}; +#endif // AIRPORT_NETWORK_HTTP_DATA_SOURCE_H + diff --git a/src/network/MessageTypes.h b/src/network/MessageTypes.h new file mode 100644 index 0000000..37ae820 --- /dev/null +++ b/src/network/MessageTypes.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include + +namespace network { + +// 位置更新消息 +struct PositionUpdateMessage { + std::string type = "position_update"; + std::string objectId; + std::string objectType; // "aircraft" 或 "vehicle" + double longitude; + double latitude; + double heading; + double speed; + uint64_t timestamp; + + nlohmann::json toJson() const { + return { + {"type", type}, + {"objectId", objectId}, + {"objectType", objectType}, + {"longitude", longitude}, + {"latitude", latitude}, + {"heading", heading}, + {"speed", speed}, + {"timestamp", timestamp} + }; + } +}; + +// 碰撞告警消息 +struct CollisionWarningMessage { + std::string type = "collision_warning"; + std::string id1; + std::string id2; + std::string warningLevel; // "low", "medium", "high" + double distance; // 当前距离 + double relativeSpeed; // 相对速度 + double threshold; // 预警距离阈值 + uint64_t timestamp; + + nlohmann::json toJson() const { + return { + {"type", type}, + {"id1", id1}, + {"id2", id2}, + {"warningLevel", warningLevel}, + {"distance", distance}, + {"relativeSpeed", relativeSpeed}, + {"threshold", threshold}, + {"timestamp", timestamp} + }; + } +}; + +struct TimeoutWarningMessage { + std::string source; // 超时来源 + int64_t elapsed_ms; // 超时时长 + bool is_read_timeout; // 是否是读取超时 + + nlohmann::json toJson() const { + return { + {"source", source}, + {"elapsed_ms", elapsed_ms}, + {"is_read_timeout", is_read_timeout} + }; + } +}; + +} // namespace network \ No newline at end of file diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h new file mode 100644 index 0000000..0cdb367 --- /dev/null +++ b/src/network/NetworkInterface.h @@ -0,0 +1,17 @@ +#ifndef AIRPORT_NETWORK_INTERFACE_H +#define AIRPORT_NETWORK_INTERFACE_H + +#include +#include +#include "types/VehicleCommand.h" + +class NetworkInterface { +public: + virtual ~NetworkInterface() = default; + virtual bool connect() = 0; + virtual void disconnect() = 0; + virtual bool send(const VehicleCommand& cmd) = 0; + virtual bool isConnected() const = 0; +}; + +#endif // AIRPORT_NETWORK_INTERFACE_H \ No newline at end of file diff --git a/src/network/TrafficLightHttpServer.cpp b/src/network/TrafficLightHttpServer.cpp new file mode 100644 index 0000000..a6805a7 --- /dev/null +++ b/src/network/TrafficLightHttpServer.cpp @@ -0,0 +1,377 @@ +#include "TrafficLightHttpServer.h" +#include "utils/Logger.h" +#include "core/System.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace network { + + namespace beast = boost::beast; + namespace http = beast::http; + namespace net = boost::asio; + using tcp = boost::asio::ip::tcp; + using json = nlohmann::json; + + // --- Forward Declarations within namespace --- + // 不再需要,因为类定义直接在下面 + // class TrafficLightSession; + // class TrafficLightListener; + + //---------------- Helper Functions for HTTP Responses ------------------ + + // Returns a bad request response + http::response bad_request(const http::request& req, beast::string_view why) { + http::response res{ http::status::bad_request, req.version() }; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = std::string(why); + res.prepare_payload(); + return res; + } + + // Returns a not found response + http::response not_found(const http::request& req) { + http::response res{ http::status::not_found, req.version() }; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "The resource '" + std::string(req.target()) + "' was not found."; + res.prepare_payload(); + return res; + } + + // Returns a server error response + http::response server_error(const http::request& req, beast::string_view what) { + http::response res{ http::status::internal_server_error, req.version() }; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + } + + // Returns a method not allowed response + http::response method_not_allowed(const http::request& req) { + http::response res{ http::status::method_not_allowed, req.version() }; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.set(http::field::allow, "POST"); + res.keep_alive(req.keep_alive()); + res.body() = "Method Not Allowed. Allowed methods: POST"; + res.prepare_payload(); + return res; + } + + // Returns a success response (200 OK) + http::response ok_response(const http::request& req, const std::string& message = "OK") { + http::response res{ http::status::ok, req.version() }; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = "{\"status\": \"success\", \"message\": \"" + message + "\"}"; + res.prepare_payload(); + return res; + } + + //---------------- Internal Session Class (Definition & Implementation) ---------------------- + // Handles an HTTP server connection. Defined entirely within the .cpp file. + class TrafficLightSession : public std::enable_shared_from_this + { + tcp::socket socket_; + beast::flat_buffer buffer_; + std::string const& doc_root_; // Keep for potential future use + http::request req_; + // Strand to ensure sequential execution of handlers for this session. + net::strand strand_; + net::io_context& ioc_; // 新增: 持有 io_context 引用 + System& system_; // 新增: System 引用 + + public: + // Take ownership of the socket + explicit TrafficLightSession(tcp::socket&& socket, std::string const& doc_root, net::io_context& ioc, System& system_ref) + : socket_(std::move(socket)), + doc_root_(doc_root), + strand_(ioc.get_executor()), + ioc_(ioc), + system_(system_ref) {} + + void run() { + net::dispatch(strand_, [self = shared_from_this()]() { + self->do_read(); + }); + } + + private: + void do_read() { + req_ = {}; // 清空请求对象以便重用 + http::async_read(socket_, buffer_, req_, + net::bind_executor(strand_, + [self = shared_from_this()](beast::error_code ec, std::size_t bytes_transferred) { + self->on_read(ec, bytes_transferred); + } + )); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + // Connection closed by peer + if (ec == http::error::end_of_stream) return do_close(); + if (ec) return Logger::error("Session Read Error: ", ec.message()); + handle_request(); + } + + void handle_request() { + // Respond to HEAD request + if (req_.method() == http::verb::head) { + auto res = ok_response(req_, ""); + res.body() = ""; // No body for HEAD + return send_response(std::move(res)); + } + + // Only allow POST requests to the specific endpoint + if (req_.method() != http::verb::post) { + return send_response(method_not_allowed(req_)); + } + + if (req_.target() != "/trafficlight") { + return send_response(not_found(req_)); + } + + // Attempt to parse the JSON body + try { + json body_json = json::parse(req_.body()); + + // Log the received data + Logger::info("接收到红绿灯数据:"); + std::string log_msg = "红绿灯状态: "; + bool first = true; + for (const auto& key : { "DI-11", "DI-12", "DI-13", "DI-14", "DI-15", "DI-16" }) { + if (body_json.contains(key)) { + if (!first) log_msg += ", "; + log_msg += key; + log_msg += ":"; + // Safely convert JSON value to string, handling potential non-string types + try { + log_msg += body_json[key].dump(); + } catch (const json::type_error& te) { + log_msg += "[invalid_type]"; + Logger::warning("JSON value for key ", key, " is not easily dumpable: ", te.what()); + } + + first = false; + } + } + if (first) { // If no relevant keys were found + log_msg += body_json.dump(); // Log the full body + } + Logger::info(log_msg); + + // TODO: Integrate data processing (e.g., pass to DataCollector) + // For now, just logging and sending OK. + + system_.processPushedTrafficLightData(body_json); + return send_response(ok_response(req_, "Data received and processed")); + } catch (const json::parse_error& e) { + Logger::error("JSON Parsing Error: ", e.what(), ", Body: ", req_.body()); + return send_response(bad_request(req_, "Invalid JSON format")); + } catch (const std::exception& e) { + Logger::error("Error processing request: ", e.what()); + return send_response(server_error(req_, "Internal server error processing request")); + } + } + + void send_response(http::response&& res) { + auto sp = std::make_shared>(std::move(res)); + http::async_write(socket_, *sp, + net::bind_executor(strand_, + [self = shared_from_this(), sp](beast::error_code ec, std::size_t bytes) { + self->on_write(sp->need_eof(), ec, bytes); + } + )); + } + + void on_write(bool close, beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) return Logger::error("Session Write Error: ", ec.message()); + if (close) return do_close(); + do_read(); + } + + void do_close() { + beast::error_code ec; + socket_.shutdown(tcp::socket::shutdown_send, ec); + } + }; + + //---------------- Internal Listener Class (Definition & Implementation) --------------------- + // Accepts incoming connections and launches sessions. Defined entirely within the .cpp file. + class TrafficLightListener : public std::enable_shared_from_this + { + net::io_context& ioc_; + tcp::acceptor acceptor_; + std::string const& doc_root_; + int max_connections_; // 保留这个,用于拒绝连接 + System& system_; // 新增: System 引用 + + public: + TrafficLightListener( + net::io_context& ioc, + tcp::endpoint endpoint, + int max_connections, + std::string const& doc_root, + System& system_ref) + : ioc_(ioc), + acceptor_(ioc), + doc_root_(doc_root), + max_connections_(max_connections), + system_(system_ref) { + beast::error_code ec; + + // Open the acceptor + acceptor_.open(endpoint.protocol(), ec); + if (ec) { Logger::error("Listener Open Error: ", ec.message()); throw beast::system_error{ ec }; } + + // Allow address reuse + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) { Logger::error("Listener Set Option Error: ", ec.message()); throw beast::system_error{ ec }; } + + // Bind to the server address + acceptor_.bind(endpoint, ec); + if (ec) { Logger::error("Listener Bind Error: ", ec.message()); throw beast::system_error{ ec }; } + + // Start listening for connections + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) { Logger::error("Listener Listen Error: ", ec.message()); throw beast::system_error{ ec }; } + } + + void run() { + do_accept(); + } + + void stop() { + beast::error_code ec_cancel, ec_close; + // 取消所有挂起的异步接受操作 + acceptor_.cancel(ec_cancel); + // 关闭 acceptor + acceptor_.close(ec_close); + if (ec_cancel) Logger::warning("Listener cancel error: ", ec_cancel.message()); + if (ec_close) Logger::warning("Listener close error: ", ec_close.message()); + } + + private: + void do_accept() { + acceptor_.async_accept( + net::bind_executor(acceptor_.get_executor(), + [self = shared_from_this()](beast::error_code ec, tcp::socket socket) { // socket 按值传递 + if (ec) { + if (ec == net::error::operation_aborted) { + return; // 服务器停止,不再接受 + } + Logger::error("Listener Accept Error: ", ec.message()); + // 对于其他错误,我们仍然尝试继续接受,除非 acceptor 关闭 + } else { + Logger::info("Accepted connection from ", socket.remote_endpoint()); + std::make_shared( + std::move(socket), // 从 lambda 的参数 socket 移动 + self->doc_root_, // 访问 listener 的成员 + self->ioc_, // 访问 listener 的成员 + self->system_ // 访问 listener 的成员 + )->run(); + } + + if (self->acceptor_.is_open()) { + self->do_accept(); // 继续接受下一个连接 + } + } + ) + ); + } + }; + + //---------------- TrafficLightHttpServer Implementation ------------------- + + TrafficLightHttpServer::TrafficLightHttpServer(uint16_t port, int max_connections, const std::string& doc_root, System& system_ref) + : port_(port), + doc_root_(doc_root), + max_connections_(max_connections), + ioc_(1), + system_(system_ref) {} + + TrafficLightHttpServer::~TrafficLightHttpServer() { + if (running_.load()) { + stop(); + } + } + + void TrafficLightHttpServer::start(int num_threads) { + if (running_.exchange(true)) { + Logger::warning("TrafficLightHttpServer already started."); + return; + } + // ... (确保 num_threads >= 1) + auto const address = net::ip::make_address("0.0.0.0"); + auto const port = port_; + try { + listener_ = std::make_shared( + ioc_, + tcp::endpoint{ address, port }, + max_connections_, + doc_root_, + system_ + ); + listener_->run(); + Logger::info("TrafficLightHttpServer starting on ", address.to_string(), ":", port, " with ", num_threads, " threads."); + threads_.reserve(num_threads); + for (int i = 0; i < num_threads; ++i) { + threads_.emplace_back([this] { run_ioc(); }); + } + } catch (const std::exception& e) { + Logger::error("Failed to start TrafficLightHttpServer: ", e.what()); + running_ = false; + throw; + } + } + + void TrafficLightHttpServer::stop() { + if (!running_.exchange(false)) { return; } + Logger::info("Stopping TrafficLightHttpServer..."); + net::post(ioc_, [this]() { + if (listener_) { + listener_->stop(); + listener_.reset(); + } + }); + ioc_.stop(); + for (auto& thread : threads_) { + if (thread.joinable()) { thread.join(); } + } + threads_.clear(); + Logger::info("TrafficLightHttpServer stopped."); + } + + void TrafficLightHttpServer::run_ioc() { + Logger::debug("TrafficLightHttpServer I/O thread started."); + try { + ioc_.run(); + } catch (const std::exception& e) { + Logger::error("Exception in TrafficLightHttpServer I/O thread: ", e.what()); + } catch (...) { + Logger::error("Unknown exception in TrafficLightHttpServer I/O thread."); + } + Logger::debug("TrafficLightHttpServer I/O thread finished."); + } + +} // namespace network + diff --git a/src/network/TrafficLightHttpServer.h b/src/network/TrafficLightHttpServer.h new file mode 100644 index 0000000..dd802c7 --- /dev/null +++ b/src/network/TrafficLightHttpServer.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // For uint16_t +#include + +// Forward declare Logger if header is heavy, or include if needed often +// #include "utils/Logger.h" + +// 前向声明 System 类 +class System; + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using json = nlohmann::json; + +namespace network { + + // 前向声明内部类 (如果 TrafficLightHttpServer 需要引用它) + class TrafficLightListener; + // class TrafficLightSession; // Session 通常由 Listener 创建和管理,Server 可能不需要直接引用 + + // Main server class (只保留这个类的声明) + class TrafficLightHttpServer + { + uint16_t port_; + std::string doc_root_; + int max_connections_; + net::io_context ioc_; + // 使用指向内部 Listener 实现的指针 + std::shared_ptr listener_; + std::vector threads_; + std::atomic running_{ false }; + System& system_; // 新增: System 引用 + + public: + TrafficLightHttpServer(uint16_t port, int max_connections, const std::string& doc_root, System& system_ref); + ~TrafficLightHttpServer(); + + TrafficLightHttpServer(const TrafficLightHttpServer&) = delete; + TrafficLightHttpServer& operator=(const TrafficLightHttpServer&) = delete; + + void start(int num_threads = 1); + void stop(); + + private: + void run_ioc(); + }; + +} \ No newline at end of file diff --git a/src/network/WebSocketServer.cpp b/src/network/WebSocketServer.cpp new file mode 100644 index 0000000..a4a26aa --- /dev/null +++ b/src/network/WebSocketServer.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include "network/WebSocketServer.h" +#include "utils/Logger.h" +#include + +namespace network { + + WebSocketServer::WebSocketServer(uint16_t port) + : acceptor_(ioc_, { boost::asio::ip::tcp::v4(), port }) {} + + WebSocketServer::~WebSocketServer() { + // 关闭所有连接 + std::lock_guard lock(sessions_mutex_); + for (auto& session : sessions_) { + try { + session.lock()->close(boost::beast::websocket::close_code::normal); + } catch (...) { + // 忽略关闭时的错误 + } + } + sessions_.clear(); + + // 停止 io_context + ioc_.stop(); + } + + void WebSocketServer::start() { + Logger::info("WebSocket 服务器启动,监听端口: ", acceptor_.local_endpoint().port()); + handleAccept(); + ioc_.run(); + } + + void WebSocketServer::broadcast(const std::string& message) { + std::lock_guard lock(sessions_mutex_); + auto it = sessions_.begin(); + int successCount = 0; + int failCount = 0; + + while (it != sessions_.end()) { + if (auto session = it->lock()) { // 获取 shared_ptr + try { + session->write(boost::asio::buffer(message)); + ++successCount; + ++it; + } catch (...) { + Logger::warning("广播消息到客户端失败"); + ++failCount; + it = sessions_.erase(it); // 移除失败的会话 + } + } else { + Logger::debug("移除失效的会话"); + it = sessions_.erase(it); // 移除已失效的会话 + } + } + + Logger::debug("广播消息完成: ", successCount, " 个成功, ", failCount, " 个失败, 当前共 ", sessions_.size(), " 个连接"); + } + + void WebSocketServer::handleAccept() { + acceptor_.async_accept( + [this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) { + if (!ec) { + Logger::info("接受新的 WebSocket 连接: ", socket.remote_endpoint().address().to_string()); + + // 创建新的 WebSocket 会话 + auto ws = std::make_shared>(std::move(socket)); + + // 异步完成 WebSocket 握手 + ws->async_accept( + [this, ws](boost::system::error_code ec) { + if (!ec) { + // 握手成功,保存会话并开始读取消息 + { + std::lock_guard lock(sessions_mutex_); + sessions_.push_back(ws); // 存储 weak_ptr + Logger::info("WebSocket 握手成功,当前连接数: ", sessions_.size()); + } + doRead(ws); + } else { + Logger::error("WebSocket 握手失败: ", ec.message()); + } + }); + } else { + Logger::error("接受 WebSocket 连接失败: ", ec.message()); + } + + // 继续接受新的连接 + handleAccept(); + }); + } + + void WebSocketServer::doRead(std::shared_ptr> ws) { + // 为每个会话创建一个专用的缓冲区 + auto buffer = std::make_shared(); + + // 异步读取消息 + ws->async_read( + *buffer, + [this, ws, buffer = buffer](boost::system::error_code ec, std::size_t bytes_transferred) { + if (!ec) { + // 成功读取消息,继续读取下一条 + buffer->consume(buffer->size()); // 清空缓冲区 + doRead(ws); + } else { + // 发生错误或连接关闭,移除会话 + std::lock_guard lock(sessions_mutex_); + auto it = std::find_if(sessions_.begin(), sessions_.end(), + [ws](const std::weak_ptr>& weak) { + return !weak.expired() && weak.lock() == ws; + }); + if (it != sessions_.end()) { + sessions_.erase(it); + } + } + }); + } + + void WebSocketServer::stop() { + running_ = false; + Logger::info("正在停止 WebSocket 服务器..."); + ioc_.stop(); // 停止 io_context + + // 关闭所有连接 + std::lock_guard lock(sessions_mutex_); + for (auto& session : sessions_) { + if (auto ws = session.lock()) { + try { + ws->close(boost::beast::websocket::close_code::normal); + Logger::debug("关闭 WebSocket 连接"); + } catch (...) { + Logger::warning("关闭 WebSocket 连接时发生错误"); + } + } + } + sessions_.clear(); + Logger::info("WebSocket 服务器已停止"); + } + +} // namespace network \ No newline at end of file diff --git a/src/network/WebSocketServer.h b/src/network/WebSocketServer.h new file mode 100644 index 0000000..426be69 --- /dev/null +++ b/src/network/WebSocketServer.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace network { + +class WebSocketServer { +public: + WebSocketServer(uint16_t port); + ~WebSocketServer(); + + void start(); + void broadcast(const std::string& message); + void stop(); + +private: + void handleAccept(); + void doRead(std::shared_ptr> ws); + + boost::asio::io_context ioc_; + boost::asio::ip::tcp::acceptor acceptor_; + std::vector>> sessions_; + std::mutex sessions_mutex_; + std::atomic running_{true}; +}; + +} // namespace network \ No newline at end of file diff --git a/src/spatial/CoordinateConverter.cpp b/src/spatial/CoordinateConverter.cpp new file mode 100644 index 0000000..7081d49 --- /dev/null +++ b/src/spatial/CoordinateConverter.cpp @@ -0,0 +1,52 @@ +#include "spatial/CoordinateConverter.h" +#include "config/SystemConfig.h" +#include "utils/Logger.h" +#include + +// 地球半径(米) +const double EARTH_RADIUS = 6378137.0; + +CoordinateConverter::CoordinateConverter() noexcept { + const auto& refPoint = SystemConfig::instance().airport.reference_point; + setReferencePoint(refPoint.latitude, refPoint.longitude); +} + +void CoordinateConverter::setReferencePoint(double lat, double lon) noexcept { + ref_lat_ = lat * M_PI / 180.0; // 转换为弧度 + ref_lon_ = lon * M_PI / 180.0; + cos_ref_lat_ = std::cos(ref_lat_); // 预计算参考点的余弦值 +} + +Vector2D CoordinateConverter::toLocalXY(double lat, double lon) const noexcept { + // 将经纬度转换为弧度 + double lat_rad = lat * M_PI / 180.0; + double lon_rad = lon * M_PI / 180.0; + + // 计算相对于参考点的经纬度差(弧度) + double d_lon = lon_rad - ref_lon_; + double d_lat = lat_rad - ref_lat_; + + // 计算东西方向距离(x)和南北方向距离(y) + Vector2D result; + result.x = EARTH_RADIUS * cos_ref_lat_ * d_lon; // 使用预计算的余弦值 + result.y = EARTH_RADIUS * d_lat; + + return result; +} + +GeoPosition CoordinateConverter::toLatLon(const Vector2D& pos) const noexcept { + // 计算经纬度差(弧度) + double d_lon = pos.x / (EARTH_RADIUS * cos_ref_lat_); // 使用预计算的余弦值 + double d_lat = pos.y / EARTH_RADIUS; + + // 计算实际经纬度(弧度) + double lon_rad = d_lon + ref_lon_; + double lat_rad = d_lat + ref_lat_; + + // 转换为度 + GeoPosition result; + result.latitude = lat_rad * 180.0 / M_PI; + result.longitude = lon_rad * 180.0 / M_PI; + + return result; +} \ No newline at end of file diff --git a/src/spatial/CoordinateConverter.h b/src/spatial/CoordinateConverter.h new file mode 100644 index 0000000..cc78141 --- /dev/null +++ b/src/spatial/CoordinateConverter.h @@ -0,0 +1,39 @@ +#ifndef AIRPORT_SPATIAL_COORDINATE_CONVERTER_H +#define AIRPORT_SPATIAL_COORDINATE_CONVERTER_H + +#include "types/BasicTypes.h" +#include +#include + +class CoordinateConverter { +public: + // 删除拷贝构造和赋值操作 + CoordinateConverter(const CoordinateConverter&) = delete; + CoordinateConverter& operator=(const CoordinateConverter&) = delete; + + // 获取全局单例 + static CoordinateConverter& instance() { + static CoordinateConverter instance; + return instance; + } + + // 设置参考点(经纬度) + void setReferencePoint(double lat, double lon) noexcept; + + // 将经纬度转换为本地平面坐标系(米) + Vector2D toLocalXY(double lat, double lon) const noexcept; + + // 将本地平面坐标转换为经纬度 + GeoPosition toLatLon(const Vector2D& pos) const noexcept; + +private: + // 私有构造函数 + CoordinateConverter() noexcept; + + static constexpr double EARTH_RADIUS = 6378137.0; // WGS84椭球体赤道半径(米) + double ref_lat_{0.0}; // 参考点纬度(弧度) + double ref_lon_{0.0}; // 参考点经度(弧度) + double cos_ref_lat_{1.0}; // 参考点纬度的余弦值(预计算) +}; + +#endif // AIRPORT_SPATIAL_COORDINATE_CONVERTER_H \ No newline at end of file diff --git a/src/spatial/QuadTree.cpp b/src/spatial/QuadTree.cpp new file mode 100644 index 0000000..61e7645 --- /dev/null +++ b/src/spatial/QuadTree.cpp @@ -0,0 +1,5 @@ +#include "QuadTree.h" + +// 显式实例化常用类型 +template class QuadTree; +template class QuadTree; \ No newline at end of file diff --git a/src/spatial/QuadTree.h b/src/spatial/QuadTree.h new file mode 100644 index 0000000..472d016 --- /dev/null +++ b/src/spatial/QuadTree.h @@ -0,0 +1,196 @@ +#ifndef AIRPORT_SPATIAL_QUAD_TREE_H +#define AIRPORT_SPATIAL_QUAD_TREE_H + +#include "types/BasicTypes.h" +#include +#include +#include + +// 边界定义 +struct Bounds { + double x; // 左上角 x 坐标 + double y; // 左上角 y 坐标 + double width; // 宽度 + double height; // 高度 + + Bounds() = default; + + // 使用左上角点和宽高构造 + Bounds(double x_, double y_, double width_, double height_) + : x(x_), y(y_), width(width_), height(height_) {} + + // 检查点是否在边界内 + bool contains(const Vector2D& point) const { + return point.x >= x && point.x <= (x + width) && + point.y >= y && point.y <= (y + height); + } + + // 检查是否与另一个边界相交 + bool intersects(const Bounds& other) const { + return !(other.x > (x + width) || (other.x + other.width) < x || + other.y > (y + height) || (other.y + other.height) < y); + } + + // 获取中心点 + Vector2D getCenter() const { + return {x + width/2, y + height/2}; + } +}; + +template +class QuadTree { +public: + explicit QuadTree(const Bounds& bounds, int capacity = 4) + : bounds_(bounds), capacity_(capacity) {} + + const Bounds& getBounds() const { return bounds_; } + + void insert(const T& item) { insertInternal(item); } + void insert(T&& item) { insertInternal(std::move(item)); } + + std::vector queryRange(const Bounds& range) const { + std::vector found; + if (!bounds_.intersects(range)) { + return found; + } + + for (const auto& item : items_) { + if (range.contains(getPosition(item))) { + found.push_back(item); + } + } + + if (divided_) { + auto nw = northwest_->queryRange(range); + auto ne = northeast_->queryRange(range); + auto sw = southwest_->queryRange(range); + auto se = southeast_->queryRange(range); + + found.insert(found.end(), nw.begin(), nw.end()); + found.insert(found.end(), ne.begin(), ne.end()); + found.insert(found.end(), sw.begin(), sw.end()); + found.insert(found.end(), se.begin(), se.end()); + } + + return found; + } + + std::vector queryNearby(const Vector2D& point, double radius) const { + // 创建一个以point为中心,边长为radius*2的正方形边界 + Bounds range( + point.x - radius, // x + point.y - radius, // y + radius * 2, // width + radius * 2 // height + ); + + return queryRange(range); + } + + void clear() { + items_.clear(); + divided_ = false; + northwest_.reset(); + northeast_.reset(); + southwest_.reset(); + southeast_.reset(); + } + +private: + Bounds bounds_; + int capacity_; + std::vector items_; + bool divided_ = false; + + std::unique_ptr northwest_; + std::unique_ptr northeast_; + std::unique_ptr southwest_; + std::unique_ptr southeast_; + + template + void insertInternal(U&& item) { + const Vector2D point = getPosition(item); + + // 如果点不在当前树的边界内,忽略该对象 + if (!bounds_.contains(point)) { + return; + } + + // 叶子节点:未分裂且未达到容量,直接存放 + if (!divided_ && items_.size() < static_cast(capacity_)) { + items_.emplace_back(std::forward(item)); + return; + } + + // 达到容量后分裂,并将已有 items_ 重新分发到子节点 + if (!divided_) { + subdivide(); + + auto oldItems = std::move(items_); + items_.clear(); + items_.shrink_to_fit(); + + for (auto& old : oldItems) { + insertIntoChildren(std::move(old)); + } + } + + insertIntoChildren(std::forward(item)); + } + + template + void insertIntoChildren(U&& item) { + const Vector2D point = getPosition(item); + + // 由于 contains 使用 <= 边界,点可能落在多象限边界上;这里固定按顺序选择一个子象限 + if (northwest_ && northwest_->bounds_.contains(point)) { + northwest_->insertInternal(std::forward(item)); + return; + } + if (northeast_ && northeast_->bounds_.contains(point)) { + northeast_->insertInternal(std::forward(item)); + return; + } + if (southwest_ && southwest_->bounds_.contains(point)) { + southwest_->insertInternal(std::forward(item)); + return; + } + if (southeast_ && southeast_->bounds_.contains(point)) { + southeast_->insertInternal(std::forward(item)); + return; + } + + // 理论上不会走到这里(已检查 bounds_ contains),兜底放回当前节点 + items_.emplace_back(std::forward(item)); + } + + void subdivide() { + double w = bounds_.width / 2; + double h = bounds_.height / 2; + + // 创建四个子区域 + Bounds nw(bounds_.x, bounds_.y, w, h); + northwest_ = std::make_unique(nw, capacity_); + + Bounds ne(bounds_.x + w, bounds_.y, w, h); + northeast_ = std::make_unique(ne, capacity_); + + Bounds sw(bounds_.x, bounds_.y + h, w, h); + southwest_ = std::make_unique(sw, capacity_); + + Bounds se(bounds_.x + w, bounds_.y + h, w, h); + southeast_ = std::make_unique(se, capacity_); + + divided_ = true; + } + + static Vector2D getPosition(const T& item) { + if constexpr (std::is_same_v || std::is_same_v) { + return item.position; // 直接返回 position 成员 + } else { + return item.getPosition(); // 对于其他类型,假设有getPosition方法 + } + } +}; + +#endif // AIRPORT_SPATIAL_QUAD_TREE_H \ No newline at end of file diff --git a/src/types/BasicTypes.cpp b/src/types/BasicTypes.cpp new file mode 100644 index 0000000..79c7aec --- /dev/null +++ b/src/types/BasicTypes.cpp @@ -0,0 +1,151 @@ +#include "types/BasicTypes.h" +#include "utils/Logger.h" +#include "spatial/CoordinateConverter.h" +#include + +double Vector2D::magnitude() const { + return std::sqrt(x * x + y * y); +} + +double Vector2D::direction() const { + double angle = std::atan2(x, y) * 180.0 / M_PI; + if (angle < 0) { + angle += 360.0; + } + return angle; +} + +double MovingObject::calculateDistance(const GeoPosition& pos1, const GeoPosition& pos2) { + // 使用 Haversine 公式计算实际距离 + const double EARTH_RADIUS = 6371000.0; // 地球半径(米) + + // 转换为弧度 + double lat1 = pos1.latitude * M_PI / 180.0; + double lon1 = pos1.longitude * M_PI / 180.0; + double lat2 = pos2.latitude * M_PI / 180.0; + double lon2 = pos2.longitude * M_PI / 180.0; + + // 计算差值 + double dlat = lat2 - lat1; + double dlon = lon2 - lon1; + + // Haversine 公式 + double a = std::sin(dlat/2) * std::sin(dlat/2) + + std::cos(lat1) * std::cos(lat2) * + std::sin(dlon/2) * std::sin(dlon/2); + double c = 2 * std::atan2(std::sqrt(a), std::sqrt(1-a)); + + return EARTH_RADIUS * c; +} + +double MovingObject::calculateHeading(const GeoPosition& from, const GeoPosition& to) { + // 计算经纬度差值 + double dlon = to.longitude - from.longitude; + double dlat = to.latitude - from.latitude; + + // 将经度差转换为实际距离(考虑纬度影响) + double lat_rad = from.latitude * M_PI / 180.0; + double dx = dlon * std::cos(lat_rad); // 东西方向的距离(经度) + double dy = dlat; // 南北方向的距离(纬度) + + // 计算航向角(正北为0度,顺时针增加) + double angle = std::atan2(dx, dy) * 180.0 / M_PI; + if (angle < 0) { + angle += 360.0; + } + + return angle; +} + +bool Aircraft::isValidPosition(const GeoPosition& newPos) const { + if (positionHistory.empty()) { + return true; + } + + double distance = calculateDistance(positionHistory.back().geo, newPos); + return distance <= MAX_POSITION_JUMP; +} + +bool Aircraft::isValidSpeed(double speed) const { + return speed <= MAX_SPEED; +} + +bool Vehicle::isValidPosition(const GeoPosition& newPos) const { + if (positionHistory.empty()) { + return true; + } + + double distance = calculateDistance(positionHistory.back().geo, newPos); + return distance <= MAX_POSITION_JUMP; +} + +bool Vehicle::isValidSpeed(double speed) const { + return speed <= MAX_SPEED; +} + +void MovingObject::updateMotion(const GeoPosition& newPos, uint64_t newTime) { + // 检查是否是新数据 + bool isNewData = positionHistory.empty() || newTime > positionHistory.back().timestamp; + if (isNewData) { + // 更新位置历史 + positionHistory.emplace_back(newPos, newTime); + if (positionHistory.size() > MAX_HISTORY) { + positionHistory.pop_front(); + } + + // 计算速度和航向 + if (positionHistory.size() >= 2) { + // 使用最近的两个点来计算速度和航向 + const auto& curr = positionHistory.back(); + const auto& prev = positionHistory[positionHistory.size() - 2]; + + // 计算距离和时间差 + double distance = calculateDistance(prev.geo, curr.geo); // 单位:米 + double timeDiff = static_cast(curr.timestamp - prev.timestamp) / 1000.0; // 转换为秒 + + // 只有当位置变化足够大且时间差足够长时才更新速度和航向 + static const double MIN_DISTANCE = 0.1; // 最小位置变化阈值(米) + static const double MIN_TIME = 0.05; // 最小时间差阈值(秒) + + if (distance > MIN_DISTANCE && timeDiff > MIN_TIME) { + // 计算新的速度 + double newSpeed = distance / timeDiff; // 米/秒 + + if (isValidSpeed(newSpeed)) { + // 使用指数移动平均来平滑速度,增大平滑因子以加快更新 + const double alpha = 0.8; // 增大平滑因子 + if (speed == 0) { + speed = newSpeed; // 第一次计算 + } else { + speed = speed * (1 - alpha) + newSpeed * alpha; // 平滑更新 + } + } + + // 计算并直接更新航向,不需要平滑处理 + heading = calculateHeading(prev.geo, curr.geo); + } + } + } + + // 更新当前位置 + geo = newPos; + timestamp = newTime; + position = CoordinateConverter::instance().toLocalXY(geo.latitude, geo.longitude); +} + +void MovingObject::copyHistoryFrom(const MovingObject& other) { + positionHistory = other.positionHistory; +} + +bool MovingObject::hasPositionChanged() const { + if (positionHistory.size() < 2) { + return true; + } + + const auto& latest = positionHistory.back(); + const auto& previous = positionHistory[positionHistory.size() - 2]; + + // 计算位置变化 + double distance = calculateDistance(latest.geo, previous.geo); + return distance > 0.1; // 位置变化超过0.1米才认为发生了变化 +} \ No newline at end of file diff --git a/src/types/BasicTypes.h b/src/types/BasicTypes.h new file mode 100644 index 0000000..fc2105f --- /dev/null +++ b/src/types/BasicTypes.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include + +// 移动物体类型 +enum class MovingObjectType { + AIRCRAFT, // 航空器 + SPECIAL, // 特勤车 + UNMANNED // 无人车(可控车辆) +}; + +// 基础数据类型 +struct Vector2D { + double x; + double y; + + Vector2D() : x(0), y(0) {} + Vector2D(double x_, double y_) : x(x_), y(y_) {} + + double magnitude() const; + double direction() const; +}; + +struct GeoPosition { + double latitude; + double longitude; +}; + +struct PositionRecord { + GeoPosition geo; + uint64_t timestamp; + + PositionRecord(const GeoPosition& g, uint64_t t) : geo(g), timestamp(t) {} +}; + +// 移动物体基类 +class MovingObject { +public: + MovingObject() : heading(0.0), speed(0.0), timestamp(0), type(MovingObjectType::SPECIAL) {} + virtual ~MovingObject() = default; + + std::string id; + Vector2D position; + GeoPosition geo; + double heading; + double speed; + int64_t timestamp; + MovingObjectType type; // 添加类型字段 + + std::deque positionHistory; + + static double calculateDistance(const GeoPosition& pos1, const GeoPosition& pos2); + static double calculateHeading(const GeoPosition& from, const GeoPosition& to); + + virtual bool isValidPosition(const GeoPosition& newPos) const = 0; + virtual bool isValidSpeed(double speed) const = 0; + virtual double getSpeedSmoothingFactor() const { return 0.3; } + + void updateMotion(const GeoPosition& newPos, uint64_t newTime); + void copyHistoryFrom(const MovingObject& other); + bool hasPositionChanged() const; + + // 添加类型识别虚函数 + virtual bool isAircraft() const { return type == MovingObjectType::AIRCRAFT; } + virtual bool isSpecialVehicle() const { return type == MovingObjectType::SPECIAL; } + virtual bool isUnmannedVehicle() const { return type == MovingObjectType::UNMANNED; } + + static constexpr size_t MAX_HISTORY = 10; +}; + +// 航空器类 +class Aircraft : public MovingObject { +public: + Aircraft() { type = MovingObjectType::AIRCRAFT; } + + std::string flightNo; + std::string trackNumber; + double altitude; + + bool isValidPosition(const GeoPosition& newPos) const override; + bool isValidSpeed(double speed) const override; + + static constexpr double MAX_SPEED = 100.0; // 最大速度(米/秒) + static constexpr double MAX_POSITION_JUMP = 50.0; // 最大位置跳变(米) + + bool isAircraft() const override { return true; } + bool isSpecialVehicle() const override { return false; } +}; + +// 车辆类 +class Vehicle : public MovingObject { +public: + Vehicle() { + type = MovingObjectType::UNMANNED; // 默认为无人车类型 + isControllable = true; // 默认为可控 + } + + std::string vehicleNo; + bool isControllable; // true 表示无人车,false 表示特勤车 + + bool isValidPosition(const GeoPosition& newPos) const override; + bool isValidSpeed(double speed) const override; + + static constexpr double MAX_SPEED = 20.0; // 最大速度(米/秒) + static constexpr double MAX_POSITION_JUMP = 10.0; // 最大位置跳变(米) + + bool isAircraft() const override { return false; } + bool isSpecialVehicle() const override { + return type == MovingObjectType::SPECIAL; // 只判断类型 + } +}; \ No newline at end of file diff --git a/src/types/TrafficLightTypes.h b/src/types/TrafficLightTypes.h new file mode 100644 index 0000000..249c26c --- /dev/null +++ b/src/types/TrafficLightTypes.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +enum class SignalStatus { + RED, + GREEN, + YELLOW, + UNKNOWN +}; + +struct TrafficLightSignal { + std::string trafficLightId; // 红绿灯设备 ID + SignalStatus ns_status = SignalStatus::UNKNOWN; // 南北向状态 + SignalStatus ew_status = SignalStatus::UNKNOWN; // 东西向状态 + uint64_t timestamp; // 时间戳 + std::string intersectionId; // 路口编号 + double latitude; // 路口纬度 + double longitude; // 路口经度 +}; \ No newline at end of file diff --git a/src/types/VehicleCommand.h b/src/types/VehicleCommand.h new file mode 100644 index 0000000..e73e0c5 --- /dev/null +++ b/src/types/VehicleCommand.h @@ -0,0 +1,60 @@ +#ifndef AIRPORT_TYPES_VEHICLE_COMMAND_H +#define AIRPORT_TYPES_VEHICLE_COMMAND_H + +#include +#include "VehicleData.h" + +// 指令类型 +enum class CommandType { + ALERT, // 告警指令 + SIGNAL, // 信号灯指令 + WARNING, // 预警指令 + RESUME, // 恢复指令 + PARKING // 安全停靠 +}; + +// 信号灯状态 +enum class SignalState { + RED, // 红灯 + GREEN, // 绿灯 + YELLOW // 黄灯 +}; + +// 指令原因 +enum class CommandReason { + TRAFFIC_LIGHT, // 红绿灯控制 + AIRCRAFT_CROSSING, // 航空器交叉 + SPECIAL_VEHICLE, // 特勤车辆 + AIRCRAFT_PUSH, // 航空器推出 + RESUME_TRAFFIC, // 恢复通行 + PARKING_SIDE // 安全停靠 +}; + +struct VehicleCommand { + std::string vehicleId; // 车辆ID + CommandType type; // 指令类型 + CommandReason reason; // 指令原因 + uint64_t timestamp; // 时间戳 + SignalState signalState; // 信号灯状态(仅当 type 为 SIGNAL 时有效) + std::string intersectionId; // 路口ID(仅当 type 为 SIGNAL 时有效) + double latitude; // 目标位置纬度(路口/航空器/特勤车) + double longitude; // 目标位置经度(路口/航空器/特勤车) + double relativeSpeed; // 相对速度(仅当 type 为 ALERT/WARNING 时有效) + double relativeMotionX; // 相对运动 X 分量(仅当 type 为 ALERT/WARNING 时有效) + double relativeMotionY; // 相对运动 Y 分量(仅当 type 为 ALERT/WARNING 时有效) + double minDistance; // 最小距离(仅当 type 为 ALERT/WARNING 时有效) + + VehicleCommand() : + type(CommandType::RESUME), + reason(CommandReason::RESUME_TRAFFIC), + timestamp(0), + signalState(SignalState::GREEN), + latitude(0), + longitude(0), + relativeSpeed(0), + relativeMotionX(0), + relativeMotionY(0), + minDistance(0) {} +}; + +#endif // AIRPORT_TYPES_VEHICLE_COMMAND_H \ No newline at end of file diff --git a/src/types/VehicleData.cpp b/src/types/VehicleData.cpp new file mode 100644 index 0000000..7ab1e45 --- /dev/null +++ b/src/types/VehicleData.cpp @@ -0,0 +1,6 @@ +#include "VehicleData.h" +#include "spatial/CoordinateConverter.h" + +void VehicleData::updateLocalPosition(const CoordinateConverter& converter) { + position = converter.toLocalXY(geo.latitude, geo.longitude); +} \ No newline at end of file diff --git a/src/types/VehicleData.h b/src/types/VehicleData.h new file mode 100644 index 0000000..3c4a863 --- /dev/null +++ b/src/types/VehicleData.h @@ -0,0 +1,30 @@ +#ifndef AIRPORT_TYPES_VEHICLE_DATA_H +#define AIRPORT_TYPES_VEHICLE_DATA_H + +#include +#include +#include "BasicTypes.h" + +// 前向声明 +class CoordinateConverter; + +enum class VehicleType { + AIRCRAFT, + VEHICLE +}; + +struct VehicleData { + std::string id; // 唯一标识 + VehicleType type; // 类型 + GeoPosition geo; // 地理坐标(经纬度) + Vector2D position; // 平面坐标(米) + Vector2D velocity; // 速度(米/秒) + double heading; // 航向角(度) + uint64_t timestamp; // 时间戳 + double altitude{0.0}; // 高度(米,仅飞行器有效) + + // 只保留声明 + void updateLocalPosition(const CoordinateConverter& converter); +}; + +#endif // AIRPORT_TYPES_VEHICLE_DATA_H \ No newline at end of file diff --git a/src/utils/Logger.cpp b/src/utils/Logger.cpp new file mode 100644 index 0000000..3a3fb36 --- /dev/null +++ b/src/utils/Logger.cpp @@ -0,0 +1,7 @@ +#include "Logger.h" + +// 显式实例化模板,以支持常用类型 +template void Logger::log(const char* level, const char* msg); +template void Logger::log(const char* level, std::string msg); +template void Logger::log(const char* level, int msg); +template void Logger::log(const char* level, double msg); \ No newline at end of file diff --git a/src/utils/Logger.h b/src/utils/Logger.h new file mode 100644 index 0000000..ac045cb --- /dev/null +++ b/src/utils/Logger.h @@ -0,0 +1,99 @@ +#ifndef AIRPORT_UTILS_LOGGER_H +#define AIRPORT_UTILS_LOGGER_H + +#include +#include +#include +#include +#include +#include + +enum class LogLevel { + DEBUG, + INFO, + WARNING, + ERROR +}; + +class Logger { +public: + static void initialize(const std::string& filename, LogLevel level = LogLevel::INFO) { + std::lock_guard lock(getMutex()); + currentLevel() = level; + logFile().open(filename, std::ios::app); + } + + static void setLogLevel(LogLevel level) { + std::lock_guard lock(getMutex()); + currentLevel() = level; + } + + template + static void debug(Args... args) { + if (currentLevel() <= LogLevel::DEBUG) { + log("DEBUG", args...); + } + } + + template + static void info(Args... args) { + if (currentLevel() <= LogLevel::INFO) { + log("INFO", args...); + } + } + + template + static void warning(Args... args) { + if (currentLevel() <= LogLevel::WARNING) { + log("WARNING", args...); + } + } + + template + static void error(Args... args) { + if (currentLevel() <= LogLevel::ERROR) { + log("ERROR", args...); + } + } + +private: + static LogLevel& currentLevel() { + static LogLevel level = LogLevel::INFO; + return level; + } + + static std::ofstream& logFile() { + static std::ofstream file; + return file; + } + + static std::mutex& getMutex() { + static std::mutex mutex; + return mutex; + } + + template + static void log(const char* level, Args... args) { + auto now = std::chrono::system_clock::now(); + auto now_c = std::chrono::system_clock::to_time_t(now); + auto now_ms = std::chrono::duration_cast( + now.time_since_epoch()) % 1000; + + std::stringstream ss; + ss << std::put_time(std::localtime(&now_c), "%Y-%m-%d %H:%M:%S") + << '.' << std::setfill('0') << std::setw(3) << now_ms.count() + << " [" << level << "] "; + + ss << std::fixed << std::setprecision(14); + (ss << ... << args) << std::endl; + + std::lock_guard lock(getMutex()); + std::cerr << ss.str(); + if (logFile().is_open()) { + logFile() << ss.str(); + logFile().flush(); + } + } +}; + +#endif // AIRPORT_UTILS_LOGGER_H \ No newline at end of file diff --git a/src/vehicle/ControllableVehicles.cpp b/src/vehicle/ControllableVehicles.cpp new file mode 100644 index 0000000..2e1ec17 --- /dev/null +++ b/src/vehicle/ControllableVehicles.cpp @@ -0,0 +1,111 @@ +#include "ControllableVehicles.h" +#include "utils/Logger.h" +#include +#include + +// 定义静态成员变量 +ControllableVehicles* ControllableVehicles::instance_ = nullptr; + +ControllableVehicles& ControllableVehicles::getInstance() { + if (instance_ == nullptr) { + instance_ = new ControllableVehicles("config/unmanned_vehicles.json"); + } + return *instance_; +} + +ControllableVehicles::ControllableVehicles(const std::string& configFile) + : http_client_(std::make_unique()) { + if (!configFile.empty()) { + loadConfig(configFile); + } +} + +ControllableVehicles::~ControllableVehicles() { + if (instance_ != nullptr) { + delete instance_; + instance_ = nullptr; + } +} + +const std::vector& ControllableVehicles::getVehicles() const { + return vehicles_; +} + +const ControllableVehicleConfig* ControllableVehicles::findVehicle(const std::string& vehicleNo) const { + auto iter = std::find_if(vehicles_.begin(), vehicles_.end(), + [&](const ControllableVehicleConfig& config) { + return config.vehicleNo == vehicleNo; + }); + + return iter != vehicles_.end() ? &(*iter) : nullptr; +} + +bool ControllableVehicles::isControllable(const std::string& vehicleNo) const { + // 查找车辆配置 + auto* config = findVehicle(vehicleNo); + if (!config) { + return false; + } + // 只有无人车类型是可控的 + return config->type == "UNMANNED"; +} + +bool ControllableVehicles::loadConfig(const std::string& configFile) { + try { + std::ifstream file(configFile); + if (!file.is_open()) { + Logger::error("Failed to open controllable vehicles config file: ", configFile); + return false; + } + + nlohmann::json jsonConfig; + file >> jsonConfig; + + for (const auto& item : jsonConfig["vehicles"]) { + ControllableVehicleConfig config; + config.vehicleNo = item["vehicleNo"].get(); + config.type = item["type"].get(); + config.ip = item["ip"].get(); + config.port = item["port"].get(); + vehicles_.push_back(config); + Logger::info("Added vehicle: ", config.vehicleNo, ", type: ", config.type); + } + + Logger::info("Loaded ", vehicles_.size(), " controllable vehicles"); + return true; + } catch (const std::exception& e) { + Logger::error("Failed to parse controllable vehicles config: ", e.what()); + return false; + } +} + +bool ControllableVehicles::sendCommand(const std::string& vehicleNo, const VehicleCommand& command) { + auto vehicle = findVehicle(vehicleNo); + if (!vehicle) { + Logger::error("Vehicle ", vehicleNo, " not found in controllable vehicles"); + return false; + } + + try { + bool success = http_client_->sendCommand(vehicle->ip, vehicle->port, command); + if (success) { + Logger::info("Successfully sent command to vehicle ", vehicleNo, ": ", + command.type == CommandType::SIGNAL ? "SIGNAL" : + command.type == CommandType::ALERT ? "ALERT" : + command.type == CommandType::WARNING ? "WARNING" : + command.type == CommandType::PARKING ? "PARKING" : "RESUME", + "/", + command.reason == CommandReason::TRAFFIC_LIGHT ? "TRAFFIC_LIGHT" : + command.reason == CommandReason::AIRCRAFT_CROSSING ? "AIRCRAFT_CROSSING" : + command.reason == CommandReason::SPECIAL_VEHICLE ? "SPECIAL_VEHICLE" : + command.reason == CommandReason::AIRCRAFT_PUSH ? "AIRCRAFT_PUSH" : + command.reason == CommandReason::PARKING_SIDE ? "PARKING_SIDE" : "RESUME_TRAFFIC"); + } else { + Logger::error("Failed to send command to vehicle ", vehicleNo); + } + return success; + } catch (const std::exception& e) { + Logger::error("Exception while sending command to vehicle ", vehicleNo, ": ", e.what()); + return false; + } +} \ No newline at end of file diff --git a/src/vehicle/ControllableVehicles.h b/src/vehicle/ControllableVehicles.h new file mode 100644 index 0000000..d789bbd --- /dev/null +++ b/src/vehicle/ControllableVehicles.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include "types/VehicleCommand.h" +#include "network/HTTPClient.h" + +struct ControllableVehicleConfig { + std::string vehicleNo; // 车牌号 + std::string type; // 车辆类型(UNMANNED 或 SPECIAL) + std::string ip; // IP地址 + int port; // 端口号 +}; + +class ControllableVehicles { +private: + static ControllableVehicles* instance_; + ControllableVehicles(const std::string& configFile); + ControllableVehicles(const ControllableVehicles&) = delete; + ControllableVehicles& operator=(const ControllableVehicles&) = delete; + ~ControllableVehicles(); + + std::vector vehicles_; + std::unique_ptr http_client_; + bool loadConfig(const std::string& configFile); + +public: + static ControllableVehicles& getInstance(); + const std::vector& getVehicles() const; + const ControllableVehicleConfig* findVehicle(const std::string& vehicleNo) const; + bool isControllable(const std::string& vehicleNo) const; + bool sendCommand(const std::string& vehicleNo, const VehicleCommand& command); +}; \ No newline at end of file diff --git a/tests/AirportBoundsTest.cpp b/tests/AirportBoundsTest.cpp new file mode 100644 index 0000000..a5d5b32 --- /dev/null +++ b/tests/AirportBoundsTest.cpp @@ -0,0 +1,246 @@ +#include +#include +#include "config/AirportBounds.h" +#include "utils/Logger.h" +#include +#include +#include + +// 测试用的 AirportBounds 类,暴露 protected 成员以便测试 +class TestAirportBounds : public AirportBounds { +public: + using AirportBounds::AirportBounds; + using AirportBounds::toAirportCoordinate; + using AirportBounds::airportBounds_; + using AirportBounds::areaBounds_; + using AirportBounds::referencePoint_; + using AirportBounds::rotationAngle_; + + // 设置测试数据 + void setTestData(const Vector2D& refPoint, double angle, const Bounds& bounds) { + referencePoint_ = refPoint; + rotationAngle_ = angle * M_PI / 180.0; // 转换为弧度 + airportBounds_ = bounds; + } +}; + +class AirportBoundsTest : public ::testing::Test { +protected: + void SetUp() override { + // 设置日志级别 + Logger::setLogLevel(LogLevel::DEBUG); + + // 创建测试实例 + bounds_ = std::make_unique(); + + // 设置基本测试数据 + // 参考点设为原点 (0, 0) + // 旋转角度设为 90 度,便于验证 + bounds_->setTestData( + Vector2D{ 0, 0 }, // 参考点 + 90.0, // 旋转角度(度) + Bounds(-2000, -2000, 4000, 4000) // 4km x 4km 的区域 + ); + } + + // 辅助函数:检查两个点是否足够接近(考虑浮点误差) + bool pointsAreClose(const Vector2D& p1, const Vector2D& p2, double tolerance = 0.1) { + return std::abs(p1.x - p2.x) < tolerance && std::abs(p1.y - p2.y) < tolerance; + } + + std::unique_ptr bounds_; +}; + +// 测试坐标转换 - 参考点 +TEST_F(AirportBoundsTest, ReferencePointTransformation) { + // 参考点应该转换为原点 (0,0) + Vector2D result = bounds_->toAirportCoordinate(Vector2D{ 0, 0 }); + EXPECT_TRUE(pointsAreClose(result, Vector2D{ 0, 0 })) + << "参考点转换结果: (" << result.x << ", " << result.y << ")"; +} + +// 测试坐标转换 - 基本位移(90度逆时针旋转) +TEST_F(AirportBoundsTest, BasicTranslation) { + // 向右移动 100 米,逆时针旋转 90 度后,应该在 y 轴正方向 + Vector2D result = bounds_->toAirportCoordinate(Vector2D{ 100, 0 }); + EXPECT_NEAR(result.x, 0, 0.001); + EXPECT_NEAR(result.y, 100, 0.001) + << "向右100米的点逆时针旋转90度后应该在 (0, 100),实际在: (" + << result.x << ", " << result.y << ")"; + + // 向上移动 100 米,逆时针旋转 90 度后,应该在 x 轴负方向 + result = bounds_->toAirportCoordinate(Vector2D{ 0, 100 }); + EXPECT_NEAR(result.x, -100, 0.001); + EXPECT_NEAR(result.y, 0, 0.001) + << "向上100米的点逆时针旋转90度后应该在 (-100, 0),实际在: (" + << result.x << ", " << result.y << ")"; +} + +// 测试坐标转换 - 复合变换(90度逆时针旋转) +TEST_F(AirportBoundsTest, CompositeTransformation) { + // 向右上方移动 (100, 100),逆时针旋转 90 度 + Vector2D result = bounds_->toAirportCoordinate(Vector2D{ 100, 100 }); + EXPECT_NEAR(result.x, -100, 0.001); + EXPECT_NEAR(result.y, 100, 0.001) + << "点(100,100)逆时针旋转90度后应该在 (-100, 100),实际在: (" + << result.x << ", " << result.y << ")"; +} + +// 测试边界检查 - 边界内的点 +TEST_F(AirportBoundsTest, PointInBounds) { + // 测试参考点(一定在边界内) + EXPECT_TRUE(bounds_->isPointInBounds(Vector2D{ 0, 0 })) + << "参考点应该在边界内"; + + // 测试边界内的点 + EXPECT_TRUE(bounds_->isPointInBounds(Vector2D{ 1000, 1000 })) + << "距离参考点1km的点应该在边界内"; +} + +// 测试边界检查 - 边界外的点 +TEST_F(AirportBoundsTest, PointOutOfBounds) { + // 测试边界外的点 + EXPECT_FALSE(bounds_->isPointInBounds(Vector2D{ 3000, 3000 })) + << "距离参考点3km的点应该在边界外"; + + // 测试另一个边界外的点 + EXPECT_FALSE(bounds_->isPointInBounds(Vector2D{ -2500, -2500 })) + << "距离参考点2.5km的点应该在边界外"; +} + +// 测试不同旋转角度(45度逆时针旋转) +TEST_F(AirportBoundsTest, DifferentRotations) { + // 重新设置为45度逆时针旋转 + bounds_->setTestData( + Vector2D{ 0, 0 }, // 参考点 + 45.0, // 旋转角度(度) + Bounds(-2000, -2000, 4000, 4000) + ); + + // 向右移动 100 米,逆时针旋转 45 度 + Vector2D result = bounds_->toAirportCoordinate(Vector2D{ 100, 0 }); + double sqrt2_2 = std::sqrt(2.0) / 2.0; // cos(45°) = sin(45°) = √2/2 + EXPECT_NEAR(result.x, 100 * sqrt2_2, 0.001); + EXPECT_NEAR(result.y, 100 * sqrt2_2, 0.001) + << "向右100米的点逆时针旋转45度后应该在 (70.71, 70.71),实际在: (" + << result.x << ", " << result.y << ")"; + + // 向上移动 100 米,逆时针旋转 45 度 + result = bounds_->toAirportCoordinate(Vector2D{ 0, 100 }); + EXPECT_NEAR(result.x, -100 * sqrt2_2, 0.001); + EXPECT_NEAR(result.y, 100 * sqrt2_2, 0.001) + << "向上100米的点逆时针旋转45度后应该在 (-70.71, 70.71),实际在: (" + << result.x << ", " << result.y << ")"; +} + +// 测试旋转变换(30度逆时针旋转) +TEST_F(AirportBoundsTest, RotationTransformation) { + // 重新设置为30度逆时针旋转 + bounds_->setTestData( + Vector2D{ 0, 0 }, // 参考点 + 30.0, // 旋转角度(度) + Bounds(-2000, -2000, 4000, 4000) + ); + + // 在世界坐标系中向右移动1000米,逆时针旋转 30 度 + Vector2D result = bounds_->toAirportCoordinate(Vector2D{ 1000, 0 }); + double cos30 = std::cos(M_PI / 6); // cos(30°) ≈ 0.866025 + double sin30 = std::sin(M_PI / 6); // sin(30°) ≈ 0.5 + + // 在机场坐标系中的期望结果: + // x = 1000 * cos(30°) ≈ 866.025 + // y = 1000 * sin(30°) ≈ 500 + EXPECT_NEAR(result.x, 1000 * cos30, 0.1); + EXPECT_NEAR(result.y, 1000 * sin30, 0.1) + << "向右1000米的点逆时针旋转30度后应该在 (866.025, 500),实际在: (" + << result.x << ", " << result.y << ")"; + + // 在世界坐标系中向上移动1000米,逆时针旋转 30 度 + result = bounds_->toAirportCoordinate(Vector2D{ 0, 1000 }); + + // 在机场坐标系中的期望结果: + // x = -1000 * sin(30°) ≈ -500 + // y = 1000 * cos(30°) ≈ 866.025 + EXPECT_NEAR(result.x, -1000 * sin30, 0.1); + EXPECT_NEAR(result.y, 1000 * cos30, 0.1) + << "向上1000米的点逆时针旋转30度后应该在 (-500, 866.025),实际在: (" + << result.x << ", " << result.y << ")"; +} + +// 测试区域检查 +TEST_F(AirportBoundsTest, AreaCheck) { + // 重新设置为0度旋转,便于测试 + bounds_->setTestData( + Vector2D{ 0, 0 }, // 参考点 + 0.0, // 旋转角度(度) + Bounds(-2000, -2000, 4000, 4000) + ); + + // 设置测试区域 + bounds_->areaBounds_[AreaType::RUNWAY] = Bounds(-1000, -100, 2000, 200); + bounds_->areaBounds_[AreaType::TAXIWAY] = Bounds(-800, -300, 1600, 600); + + // 测试跑道上的点 + Vector2D runwayPoint{ 500, 0 }; // 跑道中心偏右500米 + EXPECT_TRUE(bounds_->isPointInArea(runwayPoint, AreaType::RUNWAY)) + << "跑道上的点应该在跑道区域内"; + + // 测试滑行道上的点 + Vector2D taxiwayPoint{ 0, 200 }; // 滑行道中心向上200米 + EXPECT_TRUE(bounds_->isPointInArea(taxiwayPoint, AreaType::TAXIWAY)) + << "滑行道上的点应该在滑行道区域内"; + + // 测试区域外的点 + Vector2D outPoint{ 3000, 3000 }; // 远离机场的点 + EXPECT_FALSE(bounds_->isPointInArea(outPoint, AreaType::RUNWAY)) + << "远离机场的点不应该在任何区域内"; + EXPECT_FALSE(bounds_->isPointInArea(outPoint, AreaType::TAXIWAY)) + << "远离机场的点不应该在任何区域内"; +} + +// 测试配置文件加载 +TEST_F(AirportBoundsTest, ConfigLoading) { + // 使用项目中的配置文件 + AirportBounds bounds("config/airport_bounds.json"); + + // 验证配置是否正确加载 + const auto& airportBounds = bounds.getAirportBounds(); + EXPECT_TRUE(airportBounds.width > 0 && airportBounds.height > 0) + << "机场边界的宽度和高度应该为正值"; + + // 验证区域配置 + const auto& runwayBounds = bounds.getAreaBounds(AreaType::RUNWAY); + EXPECT_TRUE(runwayBounds.width > 0 && runwayBounds.height > 0) + << "跑道区域的宽度和高度应该为正值"; + EXPECT_TRUE(runwayBounds.width < airportBounds.width && runwayBounds.height < airportBounds.height) + << "跑道区域应该小于机场边界"; +} + +// 测试配置文件错误处理 +TEST_F(AirportBoundsTest, ConfigErrorHandling) { + // 测试文件不存在 + EXPECT_THROW(AirportBounds("nonexistent.json"), std::runtime_error); + + // 测试配置文件格式错误 + EXPECT_THROW(AirportBounds("tests/data/invalid_airport_bounds.json"), std::runtime_error); +} + +// 测试边界条件 +TEST_F(AirportBoundsTest, EdgeCases) { + // 重新设置为0度旋转,便于测试 + bounds_->setTestData( + Vector2D{ 0, 0 }, // 参考点 + 0.0, // 旋转角度(度) + Bounds(-2000, -2000, 4000, 4000) + ); + + // 测试正好在边界上的点 + Vector2D edge{ 2000, 2000 }; // 右上角边界点 + EXPECT_TRUE(bounds_->isPointInBounds(edge)) + << "边界上的点应该被认为在边界内"; + + // 测试边界外的点 + Vector2D outside{ 2001, 2001 }; // 刚好超出边界 + EXPECT_FALSE(bounds_->isPointInBounds(outside)) + << "边界外的点应该被认为在边界外"; +} \ No newline at end of file diff --git a/tests/BasicCollisionTest.cpp b/tests/BasicCollisionTest.cpp new file mode 100644 index 0000000..1241041 --- /dev/null +++ b/tests/BasicCollisionTest.cpp @@ -0,0 +1,539 @@ +#include +#include +#include + +#include "detector/CollisionDetector.h" +#include "vehicle/ControllableVehicles.h" +#include "utils/Logger.h" +#include "config/SystemConfig.h" +#include "config/AirportBounds.h" + +// Mock ControllableVehicles 类 +class MockControllableVehicles { +public: + MockControllableVehicles() = default; + + MOCK_METHOD(bool, isControllable, (const std::string& vehicleNo), (const)); + MOCK_METHOD(const ControllableVehicleConfig*, findVehicle, (const std::string& vehicleNo), (const)); + MOCK_METHOD(void, sendCommand, (const std::string& vehicleNo, const VehicleCommand& cmd)); + + // 转换为 ControllableVehicles& 的操作符 + operator const ControllableVehicles&() const { + return ControllableVehicles::getInstance(); + } +}; + +// Mock AirportBounds 类 +class MockAirportBounds : public AirportBounds { +public: + MockAirportBounds() : AirportBounds("") { + // 设置测试区域边界 + airportBounds_ = Bounds(0, 0, 5000, 4000); + areaBounds_[AreaType::TEST_ZONE] = airportBounds_; + + // 设置测试区域配置 + AreaConfig config; + config.collision_radius = {50.0, 50.0, 25.0}; // aircraft, special, unmanned + config.height_threshold = 10.0; + config.warning_zone_radius = {100.0, 100.0, 50.0}; + config.alert_zone_radius = {50.0, 50.0, 25.0}; + areaConfigs_[AreaType::TEST_ZONE] = config; + + // 设置系统配置 + auto& system_config = SystemConfig::instance(); + system_config.collision_detection.prediction.time_window = 30.0; // 设置预测时间窗口为30秒 + } + + MOCK_METHOD(AreaType, getAreaType, (const Vector2D& position), (const)); + + const AreaConfig& getAreaConfig(AreaType type) const override { + auto it = areaConfigs_.find(type); + if (it == areaConfigs_.end()) { + throw std::runtime_error("Invalid area type"); + } + return it->second; + } +}; + +class BasicCollisionTest : public ::testing::Test { +protected: + void SetUp() override { + Logger::setLogLevel(LogLevel::DEBUG); // 设置日志级别为 DEBUG + + airportBounds_ = std::make_unique(); + mockControllableVehicles_ = std::make_unique(); + + // 设置 Mock 对象的行为 + EXPECT_CALL(*airportBounds_, getAreaType(::testing::_)) + .WillRepeatedly(::testing::Return(AreaType::TEST_ZONE)); + + // 设置 Mock ControllableVehicles 的行为 + EXPECT_CALL(*mockControllableVehicles_, isControllable(::testing::_)) + .WillRepeatedly(::testing::Return(true)); + + detector_ = std::make_unique(*airportBounds_, *mockControllableVehicles_); + } + + std::unique_ptr airportBounds_; + std::unique_ptr mockControllableVehicles_; + std::unique_ptr detector_; +}; + +// 1. 静态碰撞测试 +TEST_F(BasicCollisionTest, StaticCollision) { + // 创建两个静止且重叠的物体 + Vehicle v1; + v1.vehicleNo = "V1"; + v1.position = {100.0, 100.0}; + v1.speed = 0.0; + v1.heading = 0.0; + v1.type = MovingObjectType::UNMANNED; + + Vehicle v2; + v2.vehicleNo = "V2"; + v2.position = {120.0, 100.0}; // 距离20米,小于碰撞半径25米 + v2.speed = 0.0; + v2.heading = 0.0; + v2.type = MovingObjectType::UNMANNED; + + auto result = detector_->checkCollision(v1, v2, 30.0); + + EXPECT_TRUE(result.willCollide) << "距离小于碰撞半径的静止物体应该检测为碰撞"; + EXPECT_DOUBLE_EQ(result.timeToCollision, 0.0) << "静止物体的碰撞时间应该为0"; + EXPECT_EQ(result.type, CollisionType::STATIC) << "应该识别为静态碰撞"; + + // 增加更多验证 + EXPECT_DOUBLE_EQ(result.minDistance, 20.0) << "最小距离应该是当前距离20米"; + EXPECT_DOUBLE_EQ(result.timeToMinDistance, 0.0) << "静止物体最小距离时间应该为0"; + EXPECT_NEAR(result.collisionPoint.x, 110.0, 0.1) << "碰撞点应该在两车中点"; + EXPECT_NEAR(result.collisionPoint.y, 100.0, 0.1) << "碰撞点应该在同一水平线上"; + + // 验证物体状态 + EXPECT_NEAR(result.object1State.position.x, 100.0, 0.1); + EXPECT_NEAR(result.object1State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object1State.speed, 0.0); + EXPECT_DOUBLE_EQ(result.object1State.heading, 0.0); + + EXPECT_NEAR(result.object2State.position.x, 120.0, 0.1); + EXPECT_NEAR(result.object2State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object2State.speed, 0.0); + EXPECT_DOUBLE_EQ(result.object2State.heading, 0.0); +} + +// 2. 相向碰撞测试 +TEST_F(BasicCollisionTest, HeadOnCollision) { + // 创建两个相向运动的物体 + Vehicle v1; + v1.vehicleNo = "V1"; + v1.position = {100.0, 100.0}; + v1.speed = 10.0; + v1.heading = 90.0; // 向东 + v1.type = MovingObjectType::UNMANNED; + + Vehicle v2; + v2.vehicleNo = "V2"; + v2.position = {200.0, 100.0}; // 在v1东边100米 + v2.speed = 10.0; + v2.heading = 270.0; // 向西 + v2.type = MovingObjectType::UNMANNED; + + auto result = detector_->checkCollision(v1, v2, 30.0); + + EXPECT_TRUE(result.willCollide) << "相向运动的物体应该检测为碰撞"; + EXPECT_NEAR(result.timeToCollision, 2.5, 0.1) << "碰撞时间应该接近2.5秒((100-50)米/20米每秒)"; + EXPECT_EQ(result.type, CollisionType::HEAD_ON) << "应该识别为相向碰撞"; + EXPECT_NEAR(result.collisionPoint.x, 150.0, 0.1) << "碰撞点应该在两车中点处(100+200)/2=150"; + EXPECT_NEAR(result.collisionPoint.y, 100.0, 0.1) << "碰撞点应该在同一水平线上"; + + // 增加更多验证 + EXPECT_NEAR(result.minDistance, 50.0, 0.1) << "最小距离该是碰撞半径之和50米"; + EXPECT_NEAR(result.timeToMinDistance, 2.5, 0.1) << "最小距离时间应该等于碰撞时间"; + + // 验证物体状态 + EXPECT_NEAR(result.object1State.position.x, 125.0, 0.1); + EXPECT_NEAR(result.object1State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object1State.speed, 10.0); + EXPECT_DOUBLE_EQ(result.object1State.heading, 90.0); + + EXPECT_NEAR(result.object2State.position.x, 175.0, 0.1); + EXPECT_NEAR(result.object2State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object2State.speed, 10.0); + EXPECT_DOUBLE_EQ(result.object2State.heading, 270.0); +} + +// 3. 平行运动测试 +TEST_F(BasicCollisionTest, ParallelMotion) { + // 创建两个平行运动的物体 + Vehicle v1; + v1.vehicleNo = "V1"; + v1.position = {100.0, 100.0}; + v1.speed = 10.0; + v1.heading = 90.0; // 向东 + v1.type = MovingObjectType::UNMANNED; + + Vehicle v2; + v2.vehicleNo = "V2"; + v2.position = {100.0, 150.0}; // 在v1北边50米,大于碰撞半径 + v2.speed = 10.0; + v2.heading = 90.0; // 向东 + v2.type = MovingObjectType::UNMANNED; + + auto result = detector_->checkCollision(v1, v2, 30.0); + + EXPECT_FALSE(result.willCollide) << "平行运动且距离大于碰撞半径的物体不应该检测为碰撞"; + EXPECT_EQ(result.type, CollisionType::PARALLEL) << "应该识别为平行运动"; + + // 增加更多验证 + EXPECT_DOUBLE_EQ(result.minDistance, 50.0) << "最小距离应该是初始距离50米"; + EXPECT_DOUBLE_EQ(result.timeToMinDistance, 0.0) << "平行运动的最小距离时间应该为0"; + EXPECT_DOUBLE_EQ(result.timeToCollision, std::numeric_limits::infinity()) << "平行运动不会碰撞"; + + // 不验证碰撞点,因为不会发生碰撞 +} + +// 4. 交叉路径测试 +TEST_F(BasicCollisionTest, CrossingPaths) { + // 创建两个倾斜交叉运动的物体 + Vehicle v1; + v1.vehicleNo = "V1"; + v1.position = {100.0, 100.0}; + v1.speed = 15.0; + v1.heading = 75.0; // 向东北偏东方向运动 + v1.type = MovingObjectType::UNMANNED; + + Vehicle v2; + v2.vehicleNo = "V2"; + v2.position = {150.0, 150.0}; + v2.speed = 10.0; + v2.heading = 105.0; // 向东南偏东方向运动,与 v1 夹角为 30 度 + v2.type = MovingObjectType::UNMANNED; + + // 添加航向角度差的调试日志 + double angle_diff = std::abs(v1.heading - v2.heading); + Logger::debug("航向角度差检查:"); + Logger::debug("v1 航向: " + std::to_string(v1.heading)); + Logger::debug("v2 航向: " + std::to_string(v2.heading)); + Logger::debug("航向角度差: " + std::to_string(angle_diff)); + + auto result = detector_->checkCollision(v1, v2, 30.0); + + EXPECT_TRUE(result.willCollide) << "交叉路径的物体应该检测为碰撞"; + EXPECT_EQ(result.type, CollisionType::CROSSING) << "应该识别为交叉碰撞"; + + // 计算碰撞时间和位置: + // v1: 速度分量 (3.882, 14.489) m/s // 15 m/s * (cos(15°), sin(15°)) + // v2: 速度分量 (-2.588, 9.659) m/s // 10 m/s * (cos(75°), sin(75°)) + // 相对速度: (-6.47, -4.83) m/s + // 相对速度大小: 8.06 m/s + // 碰撞点在两车位置的中点 + double collision_time = 2.07; // 根据实际计算得到 + Vector2D collision_point = {126.34, 150.006}; // 两车位置的中点 + + EXPECT_NEAR(result.timeToCollision, collision_time, 0.1) << "考虑碰撞半径25米,碰撞时间应该接近2.07秒"; + EXPECT_NEAR(result.collisionPoint.x, collision_point.x, 0.1) << "碰撞点x坐标应该在126.34"; + EXPECT_NEAR(result.collisionPoint.y, collision_point.y, 0.1) << "碰撞点y坐标应该在150.006"; + + // 增加更多验证 + EXPECT_NEAR(result.minDistance, 50.0, 0.1) << "最小距离应该是碰撞半径之和50米"; + EXPECT_NEAR(result.timeToMinDistance, collision_time, 0.1) << "最小距离时间应该等于碰撞时间"; + + // 验证物体状态 + EXPECT_NEAR(result.object1State.position.x, 108.04, 0.1); + EXPECT_NEAR(result.object1State.position.y, 130.007, 0.1); + EXPECT_DOUBLE_EQ(result.object1State.speed, 15.0); + EXPECT_DOUBLE_EQ(result.object1State.heading, 75.0); + + EXPECT_NEAR(result.object2State.position.x, 144.64, 0.1); + EXPECT_NEAR(result.object2State.position.y, 170.005, 0.1); + EXPECT_DOUBLE_EQ(result.object2State.speed, 10.0); + EXPECT_DOUBLE_EQ(result.object2State.heading, 105.0); +} + +// 5. 垂直交叉路径测试 +TEST_F(BasicCollisionTest, PerpendicularCrossingPaths) { + // 创建两个垂直交叉运动的物体 + Vehicle v1; + v1.vehicleNo = "V1"; + v1.position = {100.0, 100.0}; + v1.speed = 10.0; + v1.heading = 90.0; // 向东运动 + v1.type = MovingObjectType::UNMANNED; + + Vehicle v2; + v2.vehicleNo = "V2"; + v2.position = {150.0, 150.0}; + v2.speed = 10.0; + v2.heading = 180.0; // 向南运动,与 v1 夹角为 90 度 + v2.type = MovingObjectType::UNMANNED; + + auto result = detector_->checkCollision(v1, v2, 30.0); + + EXPECT_TRUE(result.willCollide) << "垂直交叉路径的物体应该检测为碰撞"; + EXPECT_EQ(result.type, CollisionType::CROSSING) << "应该识别为交叉碰撞"; + + // 计算碰撞时间和位置: + // v1: 速度分量 (10.0, 0.0) m/s + // v2: 速度分量 (0.0, -10.0) m/s + // 相对速度: (-10.0, -10.0) m/s + // 相对速度大小: 14.14 m/s + // 当前距离小于安全距离时,立即判定为碰撞 + double collision_time = 0.0; // 当前距离小于安全距离,立即碰撞 + Vector2D collision_point = {125.0, 125.0}; // 两车当前位置的中点 + + EXPECT_NEAR(result.timeToCollision, collision_time, 0.1) << "当前距离小于安全距离,碰撞时间应该为0"; + EXPECT_NEAR(result.collisionPoint.x, collision_point.x, 0.1) << "碰撞点x坐标应该在125.0"; + EXPECT_NEAR(result.collisionPoint.y, collision_point.y, 0.1) << "碰撞点y坐标应该在125.0"; + + // 增加更多验证 + EXPECT_NEAR(result.minDistance, 50.0, 0.1) << "最小距离应该是碰撞半径之和50米"; + EXPECT_NEAR(result.timeToMinDistance, collision_time, 0.1) << "最小距离时间应该等于碰撞时间"; + + // 验证物体状态 + EXPECT_NEAR(result.object1State.position.x, 100.0, 0.1); + EXPECT_NEAR(result.object1State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object1State.speed, 10.0); + EXPECT_DOUBLE_EQ(result.object1State.heading, 90.0); + + EXPECT_NEAR(result.object2State.position.x, 150.0, 0.1); + EXPECT_NEAR(result.object2State.position.y, 150.0, 0.1); + EXPECT_DOUBLE_EQ(result.object2State.speed, 10.0); + EXPECT_DOUBLE_EQ(result.object2State.heading, 180.0); +} + +TEST_F(BasicCollisionTest, DivergentMotion) { + // 设置两个物体背向运动的场景 + // 物体1在(150,100),向右运动,航向90度 + // 物体2在(200,100),向右运动,航向90度 + // 两物体都在远离对方,不应该发生碰撞 + + Vehicle obj1; + obj1.vehicleNo = "V1"; + obj1.position = {150, 100}; + obj1.speed = 10; + obj1.heading = 90; // 右运动 + obj1.type = MovingObjectType::UNMANNED; + + Vehicle obj2; + obj2.vehicleNo = "V2"; + obj2.position = {300, 100}; // 增加初始距离到 150m + obj2.speed = 10; + obj2.heading = 90; // 也向右运动 + obj2.type = MovingObjectType::SPECIAL; + + auto result = detector_->checkCollision(obj1, obj2, 10.0); + + // 验证结果 + EXPECT_FALSE(result.willCollide) << "同向运动且距离大于安全距离的物体不应该发生碰撞"; + EXPECT_EQ(result.type, CollisionType::PARALLEL) << "应该识别为平行运动"; + + // 初始距离应该是 150m + EXPECT_NEAR(result.minDistance, 150.0, 0.1); + + // 由于两车都以相同速度向右运动,最小距离应该保持不变 + EXPECT_NEAR(result.timeToMinDistance, 0.0, 0.1); +} + +// 6. 追尾场景测试 +TEST_F(BasicCollisionTest, TailgatingMotion) { + // 创建两个同向运动的物体,后车速度大于前车 + Vehicle v1; // 前车 + v1.vehicleNo = "V1"; + v1.position = {60.0, 100.0}; // 前车在前方60米处 + v1.speed = 10.0; // 前车速度10m/s + v1.heading = 90.0; // 向东运动 + v1.type = MovingObjectType::UNMANNED; + + Vehicle v2; // 后车 + v2.vehicleNo = "V2"; + v2.position = {0.0, 100.0}; // 后车在原点 + v2.speed = 15.0; // 后车速度15m/s + v2.heading = 90.0; // 向东运动 + v2.type = MovingObjectType::UNMANNED; + + auto result = detector_->checkCollision(v1, v2, 30.0); + + // 验证碰撞类型 + EXPECT_EQ(result.type, CollisionType::PARALLEL) << "应该识别为平行运动"; + + // 验证会发生碰撞 + EXPECT_TRUE(result.willCollide) << "后车速度大于前车,应该预测到碰撞"; + + // 验证碰撞时间(初始距离60米,相对速度5m/s,安全距离50米,需要缩短10米,所以碰撞时间应该是2秒) + EXPECT_NEAR(result.timeToCollision, 2.0, 0.1) << "碰撞时间应该接近2秒"; + + // 验证最小距离(应该是安全距离) + EXPECT_NEAR(result.minDistance, 50.0, 0.1) << "最小距离应该是安全距离50米"; + + // 验证最小距离时间(应该等于碰撞时间) + EXPECT_NEAR(result.timeToMinDistance, 2.0, 0.1) << "最小距离时间应该等于碰撞时间"; + + // 验证碰撞点(在两车碰撞时的中点) + // 前车:初始位置 60 + 10 * 2 = 80 + // 后车:初始位置 0 + 15 * 2 = 30 + // 碰撞点应该在 (80 + 30) / 2 = 55 + EXPECT_NEAR(result.collisionPoint.x, 55.0, 0.1) << "碰撞点x坐标应该在55米处"; + EXPECT_NEAR(result.collisionPoint.y, 100.0, 0.1) << "碰撞点y坐标应该保持在100米"; + + // 验证碰撞时刻的物体状态 + // 前车位置:60 + 10 * 2 = 80 + EXPECT_NEAR(result.object1State.position.x, 80.0, 0.1); + EXPECT_NEAR(result.object1State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object1State.speed, 10.0); + EXPECT_DOUBLE_EQ(result.object1State.heading, 90.0); + + // 后车位置:0 + 15 * 2 = 30 + EXPECT_NEAR(result.object2State.position.x, 30.0, 0.1); + EXPECT_NEAR(result.object2State.position.y, 100.0, 0.1); + EXPECT_DOUBLE_EQ(result.object2State.speed, 15.0); + EXPECT_DOUBLE_EQ(result.object2State.heading, 90.0); +} + +// 6. QN002 与 AC001 交叉路径测试(实际场景) +TEST_F(BasicCollisionTest, QN002AircraftCrossing) { + // 创建 AC001 航空器,从 T7 出发 + Aircraft aircraft; + aircraft.flightNo = "AC001"; + aircraft.position = {0.0, 0.0}; // T7 点的相对坐标 + aircraft.speed = 50.0 / 3.6; // 转换为米/秒 + aircraft.heading = 45.0; // 根据 T7 到 T11 的方向计算 + aircraft.type = MovingObjectType::AIRCRAFT; + + // 创建 QN002 无人车,从 T4 到 T8 路径上的一点发 + Vehicle qn002; + qn002.vehicleNo = "QN002"; + qn002.position = {-2.0, 120.0}; // 在 T4-T8 路径上,距离 T7 约 120 米(小于检测范围 150 米) + qn002.speed = 36.0 / 3.6; // 转换为米/秒 + qn002.heading = 135.0; // 根据 T4 到 T8 的方向计算 + qn002.type = MovingObjectType::UNMANNED; + qn002.isControllable = true; + + // 检测碰撞 + auto result = detector_->checkCollision(aircraft, qn002, 30.0); + + // 验证是否检测到碰撞 + EXPECT_TRUE(result.willCollide) << "QN002 与 AC001 的交叉路径应该检测为碰撞"; + EXPECT_EQ(result.type, CollisionType::CROSSING) << "应该识别为交叉碰撞"; + + // 记录详细的测试信息 + Logger::debug("QN002-AC001 碰撞测试结果:"); + Logger::debug("碰撞类型: ", static_cast(result.type)); + Logger::debug("最小距离: ", result.minDistance, "m"); + Logger::debug("碰撞时间: ", result.timeToCollision, "s"); + Logger::debug("碰撞点: (", result.collisionPoint.x, ",", result.collisionPoint.y, ")"); + + // 验证碰撞参数 + EXPECT_GT(result.timeToCollision, 0.0) << "碰撞时间应该大于0"; + EXPECT_LT(result.timeToCollision, 30.0) << "碰撞时间应该在预测窗口内"; + EXPECT_LE(result.minDistance, 75.0) << "小距离应该小于预警距离"; + + // 验证物体状态 + EXPECT_DOUBLE_EQ(result.object1State.speed, 50.0 / 3.6); + EXPECT_DOUBLE_EQ(result.object1State.heading, 45.0); + EXPECT_DOUBLE_EQ(result.object2State.speed, 36.0 / 3.6); + EXPECT_DOUBLE_EQ(result.object2State.heading, 135.0); +} + +TEST_F(BasicCollisionTest, CrossingBeforeAndAfter) { + // 设置 Mock 对象的预期行为 + EXPECT_CALL(*mockControllableVehicles_, isControllable(::testing::StrEq("QN002"))) + .WillRepeatedly(::testing::Return(true)); + + // 创建一个航空器和一个无人车 + Aircraft aircraft; + aircraft.id = "AC001"; + aircraft.type = MovingObjectType::AIRCRAFT; + aircraft.speed = 10.0; // 10米/秒 + aircraft.heading = 90.0; // 向东 + + Vehicle vehicle; + vehicle.id = "QN002"; + vehicle.vehicleNo = "QN002"; + vehicle.type = MovingObjectType::UNMANNED; + vehicle.isControllable = true; + vehicle.speed = 10.0; // 10米/秒 + vehicle.heading = 0.0; // 向北 + + // 设置初始位置:航空器在交叉点西侧,无人车在南侧 + Vector2D crossPoint{300.0, 300.0}; // 交叉点坐标 + + // 1. 测试交叉前的情况 + // 航空器在交叉点西侧,无人车在南侧 + aircraft.position = {crossPoint.x - 80.0, crossPoint.y}; // 航空器在交叉点西侧80 + vehicle.position = {crossPoint.x, crossPoint.y - 80.0}; // 无人车在交叉点南侧80米 + + // 更新交通数据 + std::vector aircrafts = {aircraft}; + std::vector vehicles = {vehicle}; + detector_->updateTraffic(aircrafts, vehicles); + + // 检测碰撞(使用30秒的预测窗口) + auto risks = detector_->detectCollisions(); + + // 记录详细的测试信息 + Logger::debug("交叉前碰撞检测:"); + Logger::debug("航空器位置: (", aircraft.position.x, ",", aircraft.position.y, ")"); + Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")"); + Logger::debug("交叉点位置: (", crossPoint.x, ",", crossPoint.y, ")"); + + ASSERT_FALSE(risks.empty()) << "交叉前应该检测到碰撞风险"; + EXPECT_EQ(risks[0].level, RiskLevel::WARNING) << "交叉前应该是预警级别"; + + // 2. 测试交叉过程中 + // 航空器在交叉点西侧20米,无人车在南侧20米 + aircraft.position = {crossPoint.x - 20.0, crossPoint.y}; + vehicle.position = {crossPoint.x, crossPoint.y - 20.0}; + + // 更新交通数据 + aircrafts = {aircraft}; + vehicles = {vehicle}; + detector_->updateTraffic(aircrafts, vehicles); + + // 检测碰撞 + risks = detector_->detectCollisions(); + + // 记录详细的测试信息 + Logger::debug("交叉过程碰撞检测:"); + Logger::debug("航空器位置: (", aircraft.position.x, ",", aircraft.position.y, ")"); + Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")"); + + ASSERT_FALSE(risks.empty()) << "交叉过程中应该检测到碰撞风险"; + EXPECT_EQ(risks[0].level, RiskLevel::CRITICAL) << "交叉过程中应该是告警级别"; + + // 3. 测试交叉后的情况 + // 航空器已经通过交叉点并远离安全距离,无人车还在交叉点附近 + aircraft.position = {crossPoint.x + 160.0, crossPoint.y}; // 航空器在交叉点东侧160米(大于安全距离150米) + vehicle.position = {crossPoint.x, crossPoint.y - 10.0}; // 无人车还在交叉点附近 + + // 更新交通数据 + aircrafts = {aircraft}; + vehicles = {vehicle}; + detector_->updateTraffic(aircrafts, vehicles); + + // 检测碰撞 + risks = detector_->detectCollisions(); + + // 记录详细的测试信息 + Logger::debug("交叉后碰撞检测:"); + Logger::debug("航空器位置: (", aircraft.position.x, ",", aircraft.position.y, ")"); + Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")"); + + EXPECT_TRUE(risks.empty()) << "航空器通过远离安全距离后,应该解除冲突"; + + // 4. 测试无人车继续运动 + // 无人车向北移动到交叉点 + vehicle.position = {crossPoint.x, crossPoint.y}; + + // 更新交通数据 + vehicles = {vehicle}; + detector_->updateTraffic(aircrafts, vehicles); + + // 检测碰撞 + risks = detector_->detectCollisions(); + + // 记录详细的测试信息 + Logger::debug("无人车继续运动碰撞检测:"); + Logger::debug("航空器位置: (", aircraft.position.x, ",", aircraft.position.y, ")"); + Logger::debug("无人车位置: (", vehicle.position.x, ",", vehicle.position.y, ")"); + + EXPECT_TRUE(risks.empty()) << "航空器已远离,无人车继续运动不应产生新的冲突"; +} \ No newline at end of file diff --git a/tests/BasicTypesTest.cpp b/tests/BasicTypesTest.cpp new file mode 100644 index 0000000..8ab20de --- /dev/null +++ b/tests/BasicTypesTest.cpp @@ -0,0 +1,128 @@ +#include +#include "types/BasicTypes.h" +#include + +class BasicTypesTest : public ::testing::Test { +protected: + void SetUp() override { + // 测试设置 + } +}; + +// 测试 Vector2D +TEST_F(BasicTypesTest, Vector2DMagnitude) { + Vector2D v(3.0, 4.0); + EXPECT_DOUBLE_EQ(v.magnitude(), 5.0); +} + +TEST_F(BasicTypesTest, Vector2DDirection) { + Vector2D v(1.0, 1.0); // 45度角 + EXPECT_NEAR(v.direction(), 45.0, 0.001); +} + +// 测试 MovingObject 的速度计算 +class TestMovingObject : public MovingObject { +protected: + bool isValidPosition(const GeoPosition& newPos) const override { + return true; + } + + bool isValidSpeed(double speed) const override { + return speed <= 100.0; + } + + double getSpeedSmoothingFactor() const override { + return 0.3; + } +}; + +TEST_F(BasicTypesTest, MovingObjectSpeedCalculation) { + TestMovingObject obj; + + // 使用毫秒级时间戳 + uint64_t now = 1000000; + + // 第一个位置 + GeoPosition pos1{36.0, 120.0}; + obj.updateMotion(pos1, now); + EXPECT_DOUBLE_EQ(obj.speed, 0.0); // 第一个点速度应该为0 + + // 第二个位置(向东移动约90米/秒) + GeoPosition pos2{36.0, 120.001}; + double distance = obj.calculateDistance(pos1, pos2); + printf("Distance: %.2f meters\n", distance); + + obj.updateMotion(pos2, now + 1000); // 1000毫秒后 + printf("Speed: %.2f m/s\n", obj.speed); + + EXPECT_NEAR(obj.speed, 90.0, 2.0); // 使用 Haversine 公式计算的实际距离 + + // 检查航向角 + EXPECT_NEAR(obj.heading, 90.0, 0.1); // 应该朝向正东 +} + +// 测试 Aircraft +TEST_F(BasicTypesTest, AircraftValidation) { + Aircraft aircraft; + aircraft.flightNo = "CES2501"; + + // 使用毫秒级时间戳 + uint64_t now = 1000000; + + // 测试位置更新 + GeoPosition pos1{36.0, 120.0}; + aircraft.updateMotion(pos1, now); + + GeoPosition pos2{36.0, 120.001}; // 约90米 + aircraft.updateMotion(pos2, now + 2000); // 2000毫秒后 + + // 速度应该约为45米/秒 + EXPECT_NEAR(aircraft.speed, 45.0, 2.0); +} + +// 测试 Vehicle +TEST_F(BasicTypesTest, VehicleValidation) { + Vehicle vehicle; + vehicle.vehicleNo = "VEH001"; + + // 使用毫秒级时间戳 + uint64_t now = 1000000; + + // 测试位置更新 + GeoPosition pos1{36.0, 120.0}; + vehicle.updateMotion(pos1, now); + + // 使用约9米的位置变化(小于10米的跳变限制) + GeoPosition pos2{36.00008, 120.0}; // 约9米 + double distance = vehicle.calculateDistance(pos1, pos2); + printf("Vehicle distance: %.2f meters\n", distance); + + vehicle.updateMotion(pos2, now + 1000); // 1000毫秒后 + printf("Vehicle speed: %.2f m/s\n", vehicle.speed); + + // 速度应该约为9米/秒(小于20米/秒的限制) + EXPECT_NEAR(vehicle.speed, 9.0, 1.0); +} + +// 添加边界条件测试 +TEST_F(BasicTypesTest, VectorZeroMagnitude) { + Vector2D v(0.0, 0.0); + EXPECT_DOUBLE_EQ(v.magnitude(), 0.0); +} + +// 测试距离计算 +TEST_F(BasicTypesTest, DistanceCalculation) { + TestMovingObject obj; + + // 测试经度方向的距离(约90米) + GeoPosition pos1{36.0, 120.0}; + GeoPosition pos2{36.0, 120.001}; + double distance1 = obj.calculateDistance(pos1, pos2); + EXPECT_NEAR(distance1, 90.0, 1.0); + + // 测试纬度方向的距离(约22米) + GeoPosition pos3{36.0, 120.0}; + GeoPosition pos4{36.0002, 120.0}; + double distance2 = obj.calculateDistance(pos3, pos4); + EXPECT_NEAR(distance2, 22.0, 1.0); +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..a32fb0d --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,26 @@ +# 设置测试源文件 +set(TEST_SOURCES + AirportBoundsTest.cpp + BasicCollisionTest.cpp + CollisionDetectorTest.cpp + TrafficLightDetectorTest.cpp + SafetyZoneTest.cpp + BasicTypesTest.cpp + HTTPDataSourceTest.cpp + DataCollectorTest.cpp +) + +# 创建测试可执行文件 +add_executable(unit_tests ${TEST_SOURCES}) + +# 链接需要的库 +target_link_libraries(unit_tests + PRIVATE + ${PROJECT_NAME}_lib + GTest::gtest_main + GTest::gmock_main +) + +# 添加测试 +include(GoogleTest) +gtest_discover_tests(unit_tests) \ No newline at end of file diff --git a/tests/CollisionDetectorTest.cpp b/tests/CollisionDetectorTest.cpp new file mode 100644 index 0000000..6fcf5f9 --- /dev/null +++ b/tests/CollisionDetectorTest.cpp @@ -0,0 +1,322 @@ +#include "detector/CollisionDetector.h" +#include "vehicle/ControllableVehicles.h" +#include "config/AirportBounds.h" +#include +#include +#include "utils/Logger.h" +#include +#include + +// Mock ControllableVehicles 类 +class MockControllableVehicles { +public: + MockControllableVehicles() = default; + + MOCK_METHOD(bool, isControllable, (const std::string& vehicleNo), (const)); + MOCK_METHOD(const ControllableVehicleConfig*, findVehicle, (const std::string& vehicleNo), (const)); + MOCK_METHOD(void, sendCommand, (const std::string& vehicleNo, const VehicleCommand& cmd)); + + // 转换为 ControllableVehicles& 的操作符 + operator const ControllableVehicles&() const { + return ControllableVehicles::getInstance(); + } +}; + +// Mock AirportBounds 类 +class MockAirportBounds : public AirportBounds { +public: + MockAirportBounds() : AirportBounds("") { + // 设置测试区域边界 + airportBounds_ = Bounds(0, 0, 5000, 4000); + areaBounds_[AreaType::TEST_ZONE] = airportBounds_; + + // 设置测试区域配置 + AreaConfig config; + config.collision_radius = {50.0, 50.0, 25.0}; // aircraft, special, unmanned + config.height_threshold = 10.0; + config.warning_zone_radius = {100.0, 100.0, 50.0}; + config.alert_zone_radius = {50.0, 50.0, 25.0}; + areaConfigs_[AreaType::TEST_ZONE] = config; + } + + MOCK_CONST_METHOD1(getAreaType, AreaType(const Vector2D& position)); + + const AreaConfig& getAreaConfig(AreaType type) const override { + auto it = areaConfigs_.find(type); + if (it == areaConfigs_.end()) { + throw std::runtime_error("Invalid area type"); + } + return it->second; + } + + const Bounds& getAirportBounds() const override { + return airportBounds_; + } +}; + +class CollisionDetectorTest : public ::testing::Test { +protected: + void SetUp() override { + // 设置日志级别为 DEBUG + Logger::initialize("", LogLevel::DEBUG); + + // 打印当前工作目录 + std::cout << "Current working directory: " << std::filesystem::current_path() << std::endl; + + // 创建 Mock 对象 + airportBounds_ = std::make_unique(); + mockControllableVehicles_ = std::make_unique(); + + // 设置 Mock 对象的行为 + EXPECT_CALL(*airportBounds_, getAreaType(::testing::_)) + .WillRepeatedly(::testing::Return(AreaType::TEST_ZONE)); + + // 创建冲突检测器 + detector_ = std::make_unique(*airportBounds_, *mockControllableVehicles_); + } + + void TearDown() override { + Logger::initialize("", LogLevel::INFO); + } + + std::unique_ptr airportBounds_; + std::unique_ptr mockControllableVehicles_; + std::unique_ptr detector_; +}; + +// 测试可控车辆(无人车)与航空器的碰撞检测 +TEST_F(CollisionDetectorTest, UnmannedVehicleAircraftCollision) { + // 设置测试数据 + Aircraft aircraft; + aircraft.flightNo = "TEST001"; + aircraft.position = {100.0, 100.0}; + aircraft.speed = 15.0; + aircraft.heading = 90.0; // 向东行驶 + aircraft.type = MovingObjectType::AIRCRAFT; + + Vehicle vehicle; + vehicle.vehicleNo = "VEH001"; + vehicle.position = {120.0, 105.0}; // 在航空器前方偏北5米处 + vehicle.speed = 8.0; + vehicle.heading = 270.0; // 向西行驶(与航空器相向而行) + vehicle.type = MovingObjectType::UNMANNED; + vehicle.isControllable = true; + + // 测试1:相向而行且距离较近 + auto collisionResult = detector_->checkCollision(aircraft, vehicle, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "相向而行且距离较近的航空器和无人车应该检测到碰撞"; + + // 测试2:增加距离到边界值 + vehicle.position = {150.0, 105.0}; // 增加到50米距离 + collisionResult = detector_->checkCollision(aircraft, vehicle, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "在警告距离内应该检测到碰撞"; + + // 测试3:超出碰撞检测范围 + vehicle.position = {900.0, 100.0}; // 增加到800米距离 + collisionResult = detector_->checkCollision(aircraft, vehicle, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "距离较远时不应该检测到碰撞"; +} + +// 测试无人车与特勤车的碰撞检测 +TEST_F(CollisionDetectorTest, UnmannedVehicleSpecialVehicleCollision) { + Vehicle unmanned; + unmanned.vehicleNo = "VEH001"; + unmanned.position = {100.0, 100.0}; + unmanned.speed = 10.0; + unmanned.heading = 90.0; // 向东行驶 + unmanned.type = MovingObjectType::UNMANNED; + unmanned.isControllable = true; + + Vehicle special; + special.vehicleNo = "VEH002"; + special.position = {120.0, 100.0}; // 距离20米 + special.speed = 5.0; + special.heading = 270.0; // 向西行驶 + special.type = MovingObjectType::SPECIAL; + special.isControllable = false; + + // 测试1:相向而行且距离较近 + auto collisionResult = detector_->checkCollision(unmanned, special, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "相向而行且距离较近的无人车和特勤车应该检测到碰撞"; + + // 测试2:在警告距离边界 + special.position = {150.0, 100.0}; // 增加到50米距离 + collisionResult = detector_->checkCollision(unmanned, special, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "在警告距离应该检测到碰撞"; + + // 测试3:超出碰撞检测范围 + special.position = {300.0, 100.0}; // 增加到200米距离 + collisionResult = detector_->checkCollision(unmanned, special, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "超出警告距离时不应该检测到碰撞"; +} + +// 测试斜向叉路径碰撞 +TEST_F(CollisionDetectorTest, DiagonalCrossingPathsCollision) { + Vehicle v1; + v1.vehicleNo = "VEH001"; + v1.position = {100.0, 100.0}; + v1.speed = 10.0; + v1.heading = 45.0; // 北行驶 + v1.type = MovingObjectType::UNMANNED; + v1.isControllable = true; + + Vehicle v2; + v2.vehicleNo = "VEH002"; + v2.position = {150.0, 150.0}; + v2.speed = 10.0; + v2.heading = 225.0; // 向西南行驶(与v1相向) + v2.type = MovingObjectType::UNMANNED; + v2.isControllable = true; + + // 测试1:路径交叉且距离较近 + auto collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "斜向相向而行且路径交叉的车辆应该检测到碰撞"; + + // 测试2:不同速度比 + v1.speed = 5.0; // 降低速度 + v2.speed = 15.0; // 提高速度 + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "不同速度比的斜向相向车辆应该检测到碰撞"; + + // 测试3:超出碰撞检测范围 + v2.position = {300.0, 300.0}; // 增加到较远距离 + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "距离较远时不应该检测到碰撞"; +} + +// 测试垂直交叉路径碰撞 +TEST_F(CollisionDetectorTest, PerpendicularCrossingPathsCollision) { + Vehicle v1; + v1.vehicleNo = "VEH001"; + v1.position = {100.0, 100.0}; + v1.speed = 10.0; + v1.heading = 90.0; // 向东行驶 + v1.type = MovingObjectType::UNMANNED; + v1.isControllable = true; + + Vehicle v2; + v2.vehicleNo = "VEH002"; + v2.position = {120.0, 120.0}; // 在v1东北方20米处 + v2.speed = 10.0; + v2.heading = 180.0; // 向南行驶 + v2.type = MovingObjectType::UNMANNED; + v2.isControllable = true; + + // 测试1:垂直相交且距离较近 + auto collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "垂直交叉路径的车辆应该检测到碰撞"; + + // 测试2:不同速度比 + v1.speed = 5.0; // 降低速度 + v2.speed = 15.0; // 提高速度 + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "不同速度的垂直交叉路径车辆应该检测到碰撞"; + + // 测试3:超出碰撞检测范围 + v2.position = {200.0, 200.0}; // 增加到较远距离 + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "距离较远时应该检测到碰撞"; +} + +// 测试静止辆的碰撞检测 +TEST_F(CollisionDetectorTest, StationaryVehiclesCollision) { + Vehicle v1; + v1.vehicleNo = "VEH001"; + v1.position = {100.0, 100.0}; + v1.speed = 0.0; // 静止 + v1.heading = 90.0; + v1.type = MovingObjectType::UNMANNED; + v1.isControllable = true; + + Vehicle v2; + v2.vehicleNo = "VEH002"; + v2.position = {105.0, 100.0}; // 距离5米 + v2.speed = 0.0; // 静止 + v2.heading = 90.0; + v2.type = MovingObjectType::UNMANNED; + v2.isControllable = true; + + // 测试1:两个静止车辆距离小于安全距离 + auto collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "距离小于安全距离的静止车辆应该检测到碰撞"; + + // 测试2:一动一静,移动车辆接近静止车辆 + v2.position = {120.0, 100.0}; // 增加距离到20米 + v2.speed = 5.0; // 开始移动 + v2.heading = 270.0; // 向西行驶(朝向v1) + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "移动车辆接近静止车辆时应该检测到碰撞"; + + // 测试3:一动一静,移动车辆远离静止车辆 + v2.position = {160.0, 100.0}; // 增加到60米,大于安全距离(50米) + v2.speed = 5.0; // 开始移动 + v2.heading = 90.0; // 向东行驶(远离v1) + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "移动车辆远离静止车辆且距离于安全距离时不应该检测到碰撞"; +} + +// 测试同向运动的碰撞检测 +TEST_F(CollisionDetectorTest, TailgatingCollision) { + Vehicle v1; + v1.vehicleNo = "VEH001"; + v1.position = {100.0, 100.0}; + v1.speed = 10.0; + v1.heading = 90.0; // 向东行驶 + v1.type = MovingObjectType::UNMANNED; + v1.isControllable = true; + + Vehicle v2; + v2.vehicleNo = "VEH002"; + v2.position = {40.0, 100.0}; // 在v1西侧60米处(100-60=40) + v2.speed = 10.0; + v2.heading = 90.0; // 也向东行驶 + v2.type = MovingObjectType::UNMANNED; + v2.isControllable = true; + + // 测试1:同向同速 + auto collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "同向同速的车辆不应该检测到碰撞"; + + // 测试2:同向不同速(v2速度更快,会追上v1) + v2.speed = 15.0; + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "同向但速度较快的车辆追上前车时应该检测到碰撞"; + + // 测试3:同向不同速但距离较远 + v2.position = {0.0, 100.0}; // 在原点 + v2.speed = 11.0; // 减小速度差,从 15m/s 改为 11m/s + collisionResult = detector_->checkCollision(v1, v2, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "相对速度小且距离较远时不应该检测到碰撞"; +} + +// 测试航空器与静止车辆的碰撞检测 +TEST_F(CollisionDetectorTest, AircraftStationaryVehicleCollision) { + Aircraft aircraft; + aircraft.flightNo = "TEST001"; + aircraft.position = {100.0, 100.0}; + aircraft.speed = 15.0; + aircraft.heading = 90.0; // 向东行驶 + aircraft.type = MovingObjectType::AIRCRAFT; + + Vehicle vehicle; + vehicle.vehicleNo = "VEH001"; + vehicle.position = {150.0, 100.0}; // 在航空器前方50米处 + vehicle.speed = 0.0; // 静止 + vehicle.heading = 90.0; + vehicle.type = MovingObjectType::UNMANNED; + vehicle.isControllable = true; + + // 测试1:航空器接近静止车辆 + auto collisionResult = detector_->checkCollision(aircraft, vehicle, 30.0); + EXPECT_TRUE(collisionResult.willCollide) << "航空器接近静止车辆时应该检测到碰撞"; + + // 测试2:静止车辆在航空器航向偏离处 + vehicle.position = {200.0, 200.0}; // 在航空器前方偏北,距离约100米(大于安全距离75米) + collisionResult = detector_->checkCollision(aircraft, vehicle, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "航空器与不在航向上的静止车辆不应该检测到碰撞"; + + // 测试3:静止车辆在航空器后方 + vehicle.position = {0.0, 100.0}; // 在航空器后方100米(大于安全距离75米) + collisionResult = detector_->checkCollision(aircraft, vehicle, 30.0); + EXPECT_FALSE(collisionResult.willCollide) << "航空器与后方的静止车辆不应该检测到碰撞"; +} \ No newline at end of file diff --git a/tests/DataCollectorTest.cpp b/tests/DataCollectorTest.cpp new file mode 100644 index 0000000..2337aec --- /dev/null +++ b/tests/DataCollectorTest.cpp @@ -0,0 +1,906 @@ +#include +#include +#include "collector/DataCollector.h" +#include "config/AirportBounds.h" +#include "spatial/CoordinateConverter.h" +#include "utils/Logger.h" +#include "network/MessageTypes.h" +#include "core/System.h" + +// 创建一个 Mock DataSource 类 +class MockDataSource : public DataSource { +public: + MOCK_METHOD0(connect, bool()); + MOCK_METHOD0(disconnect, void()); + MOCK_CONST_METHOD0(isAvailable, bool()); + MOCK_METHOD1(fetchPositionAircraftData, bool(std::vector&)); + MOCK_METHOD1(fetchPositionVehicleData, bool(std::vector&)); + MOCK_METHOD1(fetchTrafficLightSignals, bool(std::vector&)); + MOCK_METHOD2(sendUnmannedVehicleCommand, bool(const std::string&, const VehicleCommand&)); + MOCK_METHOD2(fetchUnmannedVehicleStatus, bool(const std::string&, std::string&)); +}; + +// 创建测试专用的 AirportBounds 类 +class TestAirportBounds : public AirportBounds { +public: + using AirportBounds::AirportBounds; // 继承构造函数 + + void setBounds(const Bounds& bounds) { + this->airportBounds_ = bounds; + } + + void setReferencePoint(const Vector2D& point) { + this->referencePoint_ = point; + } + + void setRotationAngle(double angle) { + this->rotationAngle_ = angle * M_PI / 180.0; // 转换为弧度 + } + +protected: + using AirportBounds::airportBounds_; + using AirportBounds::referencePoint_; + using AirportBounds::rotationAngle_; +}; + +class DataCollectorTest : public ::testing::Test { +protected: + void SetUp() override { + // 设置日志级别 + Logger::setLogLevel(LogLevel::DEBUG); + + // 初始化系统配置 + SystemConfig::instance().load("config/system_config.json"); + + // 创建测试实例 + bounds_ = std::make_unique(); + + // 设置机场边界参数 + bounds_->setReferencePoint(Vector2D{0, 0}); // 世界坐标系中的参考点 + bounds_->setRotationAngle(68.53); // 与 airport_bounds.json 保持一致 + bounds_->setBounds(Bounds(-100, -200, 800, 400)); // 与 airport_bounds.json 保持一致 + + // 初始化坐标转换器 + const auto& refPoint = SystemConfig::instance().airport.reference_point; + CoordinateConverter::instance().setReferencePoint(refPoint.latitude, refPoint.longitude); + + collector = std::make_unique(*bounds_); + mockSource = std::make_shared<::testing::NiceMock>(); + collector->setDataSource(mockSource); + + // 初始化配置 + dataSourceConfig_.position.refresh_interval_ms = 100; + dataSourceConfig_.position.timeout_ms = 5000; // 添加连接超时配置 + dataSourceConfig_.position.read_timeout_ms = 2000; // 添加读取超时配置 + + dataSourceConfig_.vehicle.refresh_interval_ms = 100; + dataSourceConfig_.vehicle.timeout_ms = 5000; + dataSourceConfig_.vehicle.read_timeout_ms = 2000; + + dataSourceConfig_.traffic_light.refresh_interval_ms = 100; + dataSourceConfig_.traffic_light.timeout_ms = 5000; + dataSourceConfig_.traffic_light.read_timeout_ms = 2000; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 500; // 设置警告间隔 + EXPECT_TRUE(collector->initialize(dataSourceConfig_, warnConfig)); + } + + void TearDown() override { + collector.reset(); + } + + // 创建测试用的航空器 + Aircraft createTestAircraft(const std::string& id, double lat, double lon) { + Aircraft aircraft; + aircraft.id = id; + aircraft.flightNo = id; + aircraft.geo.latitude = lat; + aircraft.geo.longitude = lon; + aircraft.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + return aircraft; + } + + // 创建测试用的车辆 + Vehicle createTestVehicle(const std::string& id, double lat, double lon) { + Vehicle vehicle; + vehicle.id = id; + vehicle.vehicleNo = id; + vehicle.geo.latitude = lat; + vehicle.geo.longitude = lon; + vehicle.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + return vehicle; + } + + // 创建测试用的红绿灯信号 + TrafficLightSignal createTestTrafficLight(const std::string& id, int state) { + TrafficLightSignal signal; + signal.trafficLightId = id; + signal.ns_status = static_cast(state); + signal.ew_status = static_cast(state); + signal.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + return signal; + } + + // 创建边界内的航空器 + Aircraft createOutOfBoundsAircraft(const std::string& id) { + // 计算偏移量,考虑机场旋转角度 + double angle = 68.53 * M_PI / 180.0; // 转换为弧度 + double offset = 0.027; // 约 3km + double lat_offset = offset * std::cos(angle); + double lon_offset = offset * std::sin(angle); + + const auto& refPoint = SystemConfig::instance().airport.reference_point; + return createTestAircraft(id, refPoint.latitude + lat_offset, refPoint.longitude + lon_offset); + } + + // 创建边界内的车辆 + Vehicle createOutOfBoundsVehicle(const std::string& id) { + // 计算偏移量,考虑机场旋转角度 + double angle = 68.53 * M_PI / 180.0; // 转换为弧度 + double offset = 0.0225; // 约 2.5km + double lat_offset = -offset * std::cos(angle); // 负值表示向相反方向偏移 + double lon_offset = -offset * std::sin(angle); + + const auto& refPoint = SystemConfig::instance().airport.reference_point; + return createTestVehicle(id, refPoint.latitude + lat_offset, refPoint.longitude + lon_offset); + } + + std::unique_ptr collector; + std::shared_ptr mockSource; + std::unique_ptr bounds_; + std::unique_ptr converter_; + DataSourceConfig dataSourceConfig_; +}; + +// 修改初始化测试用例 +TEST_F(DataCollectorTest, Initialization) { + DataSourceConfig dataSourceConfig; + + // 设置位置数据配置 + dataSourceConfig.position.host = "localhost"; + dataSourceConfig.position.port = 8080; + dataSourceConfig.position.aircraft_path = "/api/getCurrentFlightPositions"; + dataSourceConfig.position.refresh_interval_ms = 1000; + dataSourceConfig.position.timeout_ms = 5000; + dataSourceConfig.position.read_timeout_ms = 2000; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 1000; + warnConfig.log_interval_ms = 2000; + + EXPECT_TRUE(collector->initialize(dataSourceConfig, warnConfig)); +} + +// 测试数据采集 +TEST_F(DataCollectorTest, DataCollectionTest) { + // 设置数据源配置 + DataSourceConfig config; + + // 设置位置数据配置 + config.position.host = "localhost"; + config.position.port = 8080; + config.position.refresh_interval_ms = 100; + config.position.timeout_ms = 1000; + config.position.read_timeout_ms = 500; + + // 设置无人车数据配置 + config.vehicle.host = "localhost"; + config.vehicle.port = 8081; + config.vehicle.refresh_interval_ms = 100; + config.vehicle.timeout_ms = 1000; + config.vehicle.read_timeout_ms = 500; + + // 设置红绿灯数据配置 + config.traffic_light.host = "localhost"; + config.traffic_light.port = 8082; + config.traffic_light.refresh_interval_ms = 100; + config.traffic_light.timeout_ms = 1000; + config.traffic_light.read_timeout_ms = 500; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 200; + + collector->initialize(config, warnConfig); + + std::vector testAircraft = { + createTestAircraft("TEST1", 36.354483470, 120.08502054), // 参考点,转换后是 (0, 0) + createTestAircraft("TEST2", 36.354483470 + 0.0002 * std::cos(68.53 * M_PI / 180.0), + 120.08502054 + 0.0002 * std::sin(68.53 * M_PI / 180.0)) // 沿机场方向偏移约 20m + }; + + std::vector testVehicles = { + createTestVehicle("VEH1", 36.354483470, 120.08502054), // 参考点,转换后是 (0, 0) + createTestVehicle("VEH2", 36.354483470 - 0.0002 * std::cos(68.53 * M_PI / 180.0), + 120.08502054 - 0.0002 * std::sin(68.53 * M_PI / 180.0)) // 沿机场方向反向偏移约 20m + }; + + std::vector testSignals = { + createTestTrafficLight("TL001", 0), + createTestTrafficLight("TL002", 1) + }; + + // 设置 Mock 数据返回 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testAircraft), + ::testing::Return(true) + )); + + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testVehicles), + ::testing::Return(true) + )); + + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testSignals), + ::testing::Return(true) + )); + + std::string testStatus = "NORMAL"; + EXPECT_CALL(*mockSource, fetchUnmannedVehicleStatus(::testing::_, ::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<1>(testStatus), + ::testing::Return(true) + )); + + // 启动数据采集 + collector->start(); + + // 等待数据更新(至少 1000ms,确保数据被采集) + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + // 获取最新数据 + auto [aircraft, vehicles, traffic_lights] = collector->getLatestData(); + auto [aircraft_ts, vehicle_ts, traffic_light_ts] = collector->getLastUpdateTimestamps(); + + // 验证数据 + EXPECT_EQ(aircraft.size(), 2); + if (!aircraft.empty()) { + EXPECT_EQ(aircraft[0].flightNo, "TEST1"); + EXPECT_EQ(aircraft[1].flightNo, "TEST2"); + } + + EXPECT_EQ(vehicles.size(), 2); + if (!vehicles.empty()) { + EXPECT_EQ(vehicles[0].vehicleNo, "VEH1"); + EXPECT_EQ(vehicles[1].vehicleNo, "VEH2"); + } + + EXPECT_EQ(traffic_lights.size(), 2); + if (!traffic_lights.empty()) { + EXPECT_EQ(traffic_lights[0].trafficLightId, "TL001"); + EXPECT_EQ(traffic_lights[1].trafficLightId, "TL002"); + } + + // 验证时间戳 + EXPECT_GT(aircraft_ts, 0); + EXPECT_GT(vehicle_ts, 0); + EXPECT_GT(traffic_light_ts, 0); + + // 停止数据采集 + collector->stop(); +} + +// 测试数据采集循环 +TEST_F(DataCollectorTest, DataCollectionLoop) { + std::vector testAircraft = { + createTestAircraft("TEST1", 36.354483470, 120.08502054) // 参考点 + }; + + std::vector testVehicles = { + createTestVehicle("VEH1", 36.354483470, 120.08502054) // 参考点 + }; + + // 设置 Mock 数据返回 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testAircraft), + ::testing::Return(true) + )); + + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testVehicles), + ::testing::Return(true) + )); + + // 启动采集 + collector->start(); + + // 等待数据采集 + std::this_thread::sleep_for(std::chrono::seconds(2)); + + // 停止采集 + collector->stop(); + + // 验证数据 + auto aircraft = collector->getAircraftData(); + EXPECT_EQ(aircraft.size(), 1); + + auto vehicles = collector->getVehicleData(); + EXPECT_EQ(vehicles.size(), 1); +} + +// 测试错误处理 +TEST_F(DataCollectorTest, ErrorHandling) { + // 设置 Mock 返回错误 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + // 启动数据采集 + collector->start(); + + // 等待数据更新(至少三个采集周期) + std::this_thread::sleep_for(std::chrono::milliseconds( + dataSourceConfig_.position.refresh_interval_ms * 3)); + + // 获取最新数据 + auto [aircraft, vehicles, traffic_lights] = collector->getLatestData(); + + // 验证数据为空 + EXPECT_TRUE(aircraft.empty()); + EXPECT_TRUE(vehicles.empty()); + + // 停止数据采集 + collector->stop(); +} + +// 测试红绿灯信号获取 +TEST_F(DataCollectorTest, TrafficLightSignalsTest) { + std::vector testSignals = { + createTestTrafficLight("TL001", 0), // RED = 0 + createTestTrafficLight("TL002", 1), // GREEN = 1 + createTestTrafficLight("TL003", 2) // YELLOW = 2 + }; + + // 设置 Mock 数据返回 + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testSignals), + ::testing::Return(true) + )); + + // 启动数据采集 + collector->start(); + + // 等待数据更新(至少三个采集周期) + std::this_thread::sleep_for(std::chrono::milliseconds( + dataSourceConfig_.position.refresh_interval_ms * 3)); + + // 验证数据 + auto signals = collector->getTrafficLightSignals(); + EXPECT_EQ(signals.size(), 3); + if (signals.size() >= 3) { + EXPECT_EQ(signals[0].trafficLightId, "TL001"); + EXPECT_EQ(signals[0].ns_status, SignalStatus::RED); // RED = 0 + EXPECT_EQ(signals[1].trafficLightId, "TL002"); + EXPECT_EQ(signals[1].ns_status, SignalStatus::GREEN); // GREEN = 1 + } + + // 停止数据采集 + collector->stop(); +} + +// 测试红绿灯信号错误处理 +TEST_F(DataCollectorTest, TrafficLightSignalsErrorTest) { + // 设置 Mock 返回错误 + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + // 启动数据采集 + collector->start(); + + // 等待数据更新(至少三个采集周期) + std::this_thread::sleep_for(std::chrono::milliseconds( + dataSourceConfig_.position.refresh_interval_ms * 3)); + + // 验证数据为空 + auto signals = collector->getTrafficLightSignals(); + EXPECT_TRUE(signals.empty()); + + // 停止数据采集 + collector->stop(); +} + +// 测试连接健康检查和超时告警 +TEST_F(DataCollectorTest, ConnectionHealthAndTimeout) { + DataSourceConfig dataSourceConfig; + + // 设置位置数据配置 + dataSourceConfig.position.host = "localhost"; + dataSourceConfig.position.port = 8080; + dataSourceConfig.position.timeout_ms = 1000; // 1秒超时 + dataSourceConfig.position.read_timeout_ms = 500; // 0.5秒读取超时 + dataSourceConfig.position.refresh_interval_ms = 100; // 100ms刷新间隔 + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 500; // 500ms告警间隔 + + collector->initialize(dataSourceConfig, warnConfig); + + // 模拟连接健康检查失败 + EXPECT_CALL(*mockSource, isAvailable()) + .WillRepeatedly(::testing::Return(false)); + + // 模拟数据获取失败 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + // 启动采集 + collector->start(); + + // 等待超过超时时间 + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + // 停止采集 + collector->stop(); + + // 验证数据为空 + auto aircraft = collector->getAircraftData(); + EXPECT_TRUE(aircraft.empty()); + auto vehicles = collector->getVehicleData(); + EXPECT_TRUE(vehicles.empty()); +} + +// 测试连接恢复 +TEST_F(DataCollectorTest, ConnectionRecovery) { + DataSourceConfig config; + + // 设置位置数据配置 + config.position.host = "localhost"; + config.position.port = 8080; + config.position.refresh_interval_ms = 100; + config.position.timeout_ms = 1000; + config.position.read_timeout_ms = 500; + + // 设置无人车数据配置 + config.vehicle.host = "localhost"; + config.vehicle.port = 8081; + config.vehicle.refresh_interval_ms = 100; + config.vehicle.timeout_ms = 1000; + config.vehicle.read_timeout_ms = 500; + + // 设置红绿灯数据配置 + config.traffic_light.host = "localhost"; + config.traffic_light.port = 8082; + config.traffic_light.refresh_interval_ms = 100; + config.traffic_light.timeout_ms = 1000; + config.traffic_light.read_timeout_ms = 500; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 200; + + collector->initialize(config, warnConfig); + + std::vector testAircraft = { + createTestAircraft("TEST1", 36.354483470, 120.08502054) // 参考点 + }; + + // 设置 Mock 行为: 前两次失败,然后成功 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillOnce(::testing::Return(false)) + .WillOnce(::testing::Return(false)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testAircraft), + ::testing::Return(true) + )); + + // 添加车辆数据的 mock 行为 + std::vector testVehicles; + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillOnce(::testing::Return(false)) + .WillOnce(::testing::Return(false)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testVehicles), + ::testing::Return(true) + )); + + // 设置无人车数据 Mock 行为 + std::string testStatus = "NORMAL"; + EXPECT_CALL(*mockSource, fetchUnmannedVehicleStatus(::testing::_, ::testing::_)) + .WillOnce(::testing::Return(false)) + .WillOnce(::testing::Return(false)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<1>(testStatus), + ::testing::Return(true) + )); + + // 设置红绿灯数据 Mock 行为 + std::vector testSignals = { + createTestTrafficLight("TL001", 0) + }; + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillOnce(::testing::Return(false)) + .WillOnce(::testing::Return(false)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testSignals), + ::testing::Return(true) + )); + + // 启动数据采集 + collector->start(); + + // 等待足够长的时间以确保连接恢复 + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + // 获取最新数据 + auto [aircraft, vehicles, traffic_lights] = collector->getLatestData(); + + // 验证数据 + EXPECT_EQ(aircraft.size(), 1); + if (!aircraft.empty()) { + EXPECT_EQ(aircraft[0].flightNo, "TEST1"); + } + + // 验证红绿灯数据 + EXPECT_EQ(traffic_lights.size(), 1); + if (!traffic_lights.empty()) { + EXPECT_EQ(traffic_lights[0].trafficLightId, "TL001"); + } + + // 停止数据采集 + collector->stop(); +} + +// 测试长连接下的数据获取 +TEST_F(DataCollectorTest, LongConnectionDataFetch) { + DataSourceConfig dataSourceConfig; + + // 设置位置数据配置 + dataSourceConfig.position.host = "localhost"; + dataSourceConfig.position.port = 8080; + dataSourceConfig.position.refresh_interval_ms = 100; + dataSourceConfig.position.timeout_ms = 1000; + dataSourceConfig.position.read_timeout_ms = 500; + + collector->initialize(dataSourceConfig, WarnConfig{}); + + std::vector testAircraft = { + createTestAircraft("TEST1", 36.354483470, 120.08502054), // 参考点,转换后是 (0, 0) + createTestAircraft("TEST2", 36.354483470 + 0.0002 * std::cos(68.53 * M_PI / 180.0), + 120.08502054 + 0.0002 * std::sin(68.53 * M_PI / 180.0)) // 沿机场方向偏移约 20m + }; + + std::vector testVehicles = { + createTestVehicle("VEH1", 36.354483470, 120.08502054), // 参考点,转换后是 (0, 0) + createTestVehicle("VEH2", 36.354483470 - 0.0002 * std::cos(68.53 * M_PI / 180.0), + 120.08502054 - 0.0002 * std::sin(68.53 * M_PI / 180.0)) // 沿机场方向反向偏移约 20m + }; + + std::vector testSignals = { + createTestTrafficLight("TL001", 0) + }; + + // 模拟连接保持可用 + EXPECT_CALL(*mockSource, isAvailable()) + .WillRepeatedly(::testing::Return(true)); + + // 模拟多次成功获取数据 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .Times(::testing::AtLeast(3)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testAircraft), + ::testing::Return(true) + )); + + // 添加其他数据源的 mock 行为 + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testVehicles), + ::testing::Return(true) + )); + + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<0>(testSignals), + ::testing::Return(true) + )); + + // 添加无人车状态获取的 mock 行为 + std::string testStatus = "NORMAL"; + EXPECT_CALL(*mockSource, fetchUnmannedVehicleStatus(::testing::_, ::testing::_)) + .WillRepeatedly(::testing::DoAll( + ::testing::SetArgReferee<1>(testStatus), + ::testing::Return(true) + )); + + // 启动采集 + collector->start(); + + // 等待多个刷新周期 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 增加等待时间确保执行次数 + + // 验证数据 + auto aircraft = collector->getAircraftData(); + EXPECT_FALSE(aircraft.empty()); + if (!aircraft.empty()) { + EXPECT_EQ(aircraft[0].flightNo, "TEST1"); + } + + auto vehicles = collector->getVehicleData(); + EXPECT_FALSE(vehicles.empty()); + if (!vehicles.empty()) { + EXPECT_EQ(vehicles[0].vehicleNo, "VEH1"); + } + + auto signals = collector->getTrafficLightSignals(); + EXPECT_FALSE(signals.empty()); + if (!signals.empty()) { + EXPECT_EQ(signals[0].trafficLightId, "TL001"); + } + + // 停止采集 + collector->stop(); +} + +TEST_F(DataCollectorTest, InitializeWithTimeoutConfig) { + DataSourceConfig config; + + // 设置位置数据配置 + config.position.host = "localhost"; + config.position.port = 8080; + config.position.timeout_ms = 5000; // 连接超时 5 秒 + config.position.read_timeout_ms = 2000; // 读取超时 2 秒 + config.position.refresh_interval_ms = 100; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 500; + + EXPECT_NO_THROW(collector->initialize(config, warnConfig)); +} + +TEST_F(DataCollectorTest, TimeoutWarningTest) { + DataSourceConfig config; + + // 设置位置数据配置 + config.position.host = "localhost"; + config.position.port = 8080; + config.position.timeout_ms = 1000; // 连接超时 1 秒 + config.position.read_timeout_ms = 500; // 读取超时 0.5 秒 + config.position.refresh_interval_ms = 100; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 200; // 警告间隔 0.2 秒 + + collector->initialize(config, warnConfig); + + // 模拟连接成功但读取超时 + EXPECT_CALL(*mockSource, isAvailable()) + .WillRepeatedly(::testing::Return(true)); + + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); // 持续读取超时 + + // 启动采集 + collector->start(); + + // 等待足够长的时间以触发多次警告 + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + + // 停止采集 + collector->stop(); +} + +// 测试超时警告机制 +TEST_F(DataCollectorTest, TimeoutWarningMechanism) { + DataSourceConfig config; + + // 设置位置数据配置 + config.position.host = "localhost"; + config.position.port = 8080; + config.position.timeout_ms = 200; // 连接超时 200ms + config.position.read_timeout_ms = 100; // 读取超时 100ms + config.position.refresh_interval_ms = 10; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 50; // 警告间隔 50ms + + // 创建一个计数器来记录收到的警告数量 + int warningCount = 0; + + // 创建一个 mock System 来捕获警告 + class MockSystem : public System { + public: + MOCK_METHOD1(broadcastTimeoutWarning, void(const network::TimeoutWarningMessage&)); + }; + + auto mockSystem = std::make_shared(); + + // 设置期望接收到超时警告 + EXPECT_CALL(*mockSystem, broadcastTimeoutWarning(::testing::_)) + .WillRepeatedly(::testing::Invoke([&warningCount](const network::TimeoutWarningMessage& warning) { + warningCount++; + // 验证警告消息的内容 + EXPECT_THAT(warning.source, ::testing::AnyOf( + ::testing::StrEq("position"), + ::testing::StrEq("unmanned"), + ::testing::StrEq("traffic_light") + )); + EXPECT_GT(warning.elapsed_ms, 0); + })); + + // 设置 mock system 到 collector + collector->setSystem(mockSystem); + collector->initialize(config, warnConfig); + + // 启动采集器 + collector->start(); + + // 模拟所有数据源获取失败 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + // 等待足够长的时间以确保警告被发送 + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // 停止采集器 + collector->stop(); + + // 验证是否收到了警告 + EXPECT_GT(warningCount, 0); +} + +// 测试读取超时机制 +TEST_F(DataCollectorTest, ReadTimeoutMechanism) { + DataSourceConfig config; + + // 设置位置数据配置 + config.position.host = "localhost"; + config.position.port = 8080; + config.position.timeout_ms = 1000; // 连接超时 1000ms + config.position.read_timeout_ms = 500; // 读取超时 500ms + config.position.refresh_interval_ms = 100; + + // 设置无人车数据配置 + config.vehicle.host = "localhost"; + config.vehicle.port = 8081; + config.vehicle.timeout_ms = 1000; + config.vehicle.read_timeout_ms = 500; + config.vehicle.refresh_interval_ms = 100; + + // 设置红绿灯数据配置 + config.traffic_light.host = "localhost"; + config.traffic_light.port = 8082; + config.traffic_light.timeout_ms = 1000; + config.traffic_light.read_timeout_ms = 500; + config.traffic_light.refresh_interval_ms = 100; + + WarnConfig warnConfig; + warnConfig.warning_interval_ms = 200; // 警告间隔 200ms + + // 创建一个计数器来记录收到的警告数量 + int warningCount = 0; + + // 创建一个 mock System 来捕获警告 + class MockSystem : public System { + public: + MOCK_METHOD1(broadcastTimeoutWarning, void(const network::TimeoutWarningMessage&)); + }; + + auto mockSystem = std::make_shared(); + + // 设置期望接收到超时警告 + EXPECT_CALL(*mockSystem, broadcastTimeoutWarning(::testing::_)) + .WillRepeatedly(::testing::Invoke([&warningCount](const network::TimeoutWarningMessage& warning) { + warningCount++; + // 验证警告消息的内容 + EXPECT_THAT(warning.source, ::testing::AnyOf( + ::testing::StrEq("position"), + ::testing::StrEq("unmanned"), + ::testing::StrEq("traffic_light") + )); + EXPECT_GT(warning.elapsed_ms, 0); + EXPECT_TRUE(warning.is_read_timeout); + })); + + // 设置 mock system 到 collector + collector->setSystem(mockSystem); + collector->initialize(config, warnConfig); + + // 启动采集器 + collector->start(); + + // 模拟所有数据源获取失败 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + EXPECT_CALL(*mockSource, fetchTrafficLightSignals(::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + EXPECT_CALL(*mockSource, fetchUnmannedVehicleStatus(::testing::_, ::testing::_)) + .WillRepeatedly(::testing::Return(false)); + + // 等待足够长的时间以确保警告被发送 + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + // 停止采集器 + collector->stop(); + + // 验证是否收到了警告 + EXPECT_GT(warningCount, 0); +} + +// 添加新的测试用例:测试边界外数据过滤 +TEST_F(DataCollectorTest, OutOfBoundsDataFiltering) { + DataSourceConfig dataSourceConfig; + WarnConfig warnConfig; + EXPECT_TRUE(collector->initialize(dataSourceConfig, warnConfig)); + + // 创建测试数据:混合边界内外的数据 + const auto& refPoint = SystemConfig::instance().airport.reference_point; + + // 创建边界内的航空器 + std::vector testAircraft = { + createTestAircraft("IN_BOUNDS_1", refPoint.latitude, refPoint.longitude), // 参考点 + createTestAircraft("IN_BOUNDS_2", + refPoint.latitude + 0.001 * std::cos(68.53 * M_PI / 180.0), // 沿机场方向偏移约 100m + refPoint.longitude + 0.001 * std::sin(68.53 * M_PI / 180.0)), + createOutOfBoundsAircraft("OUT_BOUNDS_1"), // 边界外偏移约 3km + createOutOfBoundsAircraft("OUT_BOUNDS_2") // 边界外偏移约 3km + }; + + // 创建边界内的车辆 + std::vector testVehicles = { + createTestVehicle("VEH_IN_1", refPoint.latitude, refPoint.longitude), // 参考点 + createTestVehicle("VEH_IN_2", + refPoint.latitude + 0.001 * std::cos(68.53 * M_PI / 180.0), // 沿机场方向偏移约 100m + refPoint.longitude + 0.001 * std::sin(68.53 * M_PI / 180.0)), + createOutOfBoundsVehicle("VEH_OUT_1"), // 边界外偏移约 2.5km + createOutOfBoundsVehicle("VEH_OUT_2") // 边界外偏移约 2.5km + }; + + // 设置 Mock 数据返回 + EXPECT_CALL(*mockSource, fetchPositionAircraftData(::testing::_)) + .WillOnce(::testing::DoAll( + ::testing::SetArgReferee<0>(testAircraft), + ::testing::Return(true) + )); + + EXPECT_CALL(*mockSource, fetchPositionVehicleData(::testing::_)) + .WillOnce(::testing::DoAll( + ::testing::SetArgReferee<0>(testVehicles), + ::testing::Return(true) + )); + + // 执行刷新 + collector->fetchPositionData(); + + // 验证过滤结果 + const auto& filteredAircraft = collector->getAircraftData(); + const auto& filteredVehicles = collector->getVehicleData(); + + // 检查过滤后的数量 + EXPECT_EQ(filteredAircraft.size(), 2) << "应该只保留2架在边界内的航空器"; + EXPECT_EQ(filteredVehicles.size(), 2) << "应该只保留2辆在边界内的车辆"; + + // 检查保留的是正确的航空器 + if (filteredAircraft.size() >= 2) { + EXPECT_EQ(filteredAircraft[0].flightNo, "IN_BOUNDS_1"); + EXPECT_EQ(filteredAircraft[1].flightNo, "IN_BOUNDS_2"); + } + + // 检查保留的是正确的车辆 + if (filteredVehicles.size() >= 2) { + EXPECT_EQ(filteredVehicles[0].vehicleNo, "VEH_IN_1"); + EXPECT_EQ(filteredVehicles[1].vehicleNo, "VEH_IN_2"); + } +} \ No newline at end of file diff --git a/tests/HTTPDataSourceTest.cpp b/tests/HTTPDataSourceTest.cpp new file mode 100644 index 0000000..e4e914b --- /dev/null +++ b/tests/HTTPDataSourceTest.cpp @@ -0,0 +1,286 @@ +#include "network/HTTPDataSource.h" +#include +#include +#include "utils/Logger.h" +#include +#include +#include + +class HTTPDataSourceTest : public ::testing::Test { +protected: + void SetUp() override {} + void TearDown() override {} +}; + +TEST_F(HTTPDataSourceTest, BasicFunctionality) { + DataSourceConfig config; + config.position.host = "localhost"; + config.position.port = 8081; + config.position.aircraft_path = "/openApi/getCurrentFlightPositions"; + config.position.vehicle_path = "/openApi/getCurrentVehiclePositions"; + config.position.auth.auth_required = true; + config.position.auth.auth_path = "/login"; + config.position.auth.username = "dianxin"; + config.position.auth.password = "dianxin@123"; + config.position.timeout_ms = 5000; + config.position.read_timeout_ms = 2000; + config.position.refresh_interval_ms = 100; + + HTTPDataSource source(config); + ASSERT_TRUE(source.connect()) << "连接失败"; + + std::vector aircraft; + bool success = source.fetchPositionAircraftData(aircraft); + ASSERT_TRUE(success) << "获取航空器数据失败"; + ASSERT_FALSE(aircraft.empty()) << "航空器数据为空"; + + std::vector vehicles; + ASSERT_TRUE(source.fetchPositionVehicleData(vehicles)) << "获取车辆数据失败"; + ASSERT_FALSE(vehicles.empty()) << "车辆数据为空"; +} + +TEST_F(HTTPDataSourceTest, AuthenticationFailure) { + DataSourceConfig config; + config.position.host = "localhost"; + config.position.port = 8081; + config.position.aircraft_path = "/openApi/getCurrentFlightPositions"; + config.position.vehicle_path = "/openApi/getCurrentVehiclePositions"; + config.position.refresh_interval_ms = 1000; + config.position.timeout_ms = 1000; + config.position.read_timeout_ms = 500; + + config.position.auth.auth_required = true; + config.position.auth.username = "wrong_user"; + config.position.auth.password = "wrong_pass"; + config.position.auth.auth_path = "/login"; + + HTTPDataSource source(config); + std::vector aircraft; + EXPECT_FALSE(source.fetchPositionAircraftData(aircraft)); +} + +TEST_F(HTTPDataSourceTest, Reconnection) { + DataSourceConfig config; + config.position.host = "localhost"; + config.position.port = 8081; + config.position.aircraft_path = "/openApi/getCurrentFlightPositions"; + config.position.vehicle_path = "/openApi/getCurrentVehiclePositions"; + config.position.refresh_interval_ms = 1000; + config.position.timeout_ms = 1000; + config.position.read_timeout_ms = 500; + + config.position.auth.auth_required = true; + config.position.auth.username = "dianxin"; + config.position.auth.password = "dianxin@123"; + config.position.auth.auth_path = "/login"; + + HTTPDataSource source(config); + EXPECT_TRUE(source.connect()); + + std::vector aircraft; + EXPECT_TRUE(source.fetchPositionAircraftData(aircraft)); + + source.disconnect(); + EXPECT_TRUE(source.connect()); + + aircraft.clear(); + EXPECT_TRUE(source.fetchPositionAircraftData(aircraft)); +} + +TEST_F(HTTPDataSourceTest, AuthenticationOnly) { + DataSourceConfig config; + config.position.host = "localhost"; + config.position.port = 8081; + config.position.aircraft_path = "/openApi/getCurrentFlightPositions"; + config.position.vehicle_path = "/openApi/getCurrentVehiclePositions"; + config.position.refresh_interval_ms = 1000; + config.position.timeout_ms = 1000; + config.position.read_timeout_ms = 500; + + config.position.auth.auth_required = true; + config.position.auth.username = "dianxin"; + config.position.auth.password = "dianxin@123"; + config.position.auth.auth_path = "/login"; + + HTTPDataSource source(config); + EXPECT_TRUE(source.connect()); + + std::vector aircraft; + EXPECT_TRUE(source.fetchPositionAircraftData(aircraft)); +} + +TEST_F(HTTPDataSourceTest, TrafficLightSignals) { + DataSourceConfig config; + config.position.host = "localhost"; + config.position.port = 8081; + config.position.aircraft_path = "/openApi/getCurrentFlightPositions"; + config.position.vehicle_path = "/openApi/getCurrentVehiclePositions"; + config.position.auth.auth_required = true; + config.position.auth.auth_path = "/login"; + config.position.auth.username = "dianxin"; + config.position.auth.password = "dianxin@123"; + config.position.timeout_ms = 5000; + config.position.read_timeout_ms = 2000; + config.position.refresh_interval_ms = 100; + + config.traffic_light.host = "localhost"; + config.traffic_light.port = 8081; + config.traffic_light.signal_path = "/api/VehicleCommandInfo"; + config.vehicle.host = "localhost"; + config.vehicle.port = 8081; + config.vehicle.command_path = "/api/VehicleCommandInfo"; + + HTTPDataSource source(config); + ASSERT_TRUE(source.connect()); + + // 先获取车辆状态来初始化车辆 + std::vector vehicles; + ASSERT_TRUE(source.fetchPositionVehicleData(vehicles)) << "获取车辆状态失败"; + ASSERT_FALSE(vehicles.empty()) << "车辆数据为空"; + + // 发送绿灯指令(优先级 2) + VehicleCommand green_command; + green_command.vehicleId = "QN001"; + green_command.type = CommandType::SIGNAL; + green_command.reason = CommandReason::TRAFFIC_LIGHT; + green_command.signalState = SignalState::GREEN; + green_command.intersectionId = "INTERSECTION_001"; + green_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(green_command.vehicleId, green_command)) << "发送绿灯指令失败"; + + // 等待绿灯指令生效 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 发送红灯指令(优先级 4) + VehicleCommand red_command; + red_command.vehicleId = "QN001"; + red_command.type = CommandType::SIGNAL; + red_command.reason = CommandReason::TRAFFIC_LIGHT; + red_command.signalState = SignalState::RED; + red_command.intersectionId = "INTERSECTION_001"; + red_command.latitude = 36.36; + red_command.longitude = 120.08; + red_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(red_command.vehicleId, red_command)) << "发送红灯指令失败"; + + // 等待红灯指令生效 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + + // 最后发送绿灯指令 + green_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(green_command.vehicleId, green_command)) << "发送最终绿灯指令失败"; +} + +TEST_F(HTTPDataSourceTest, UnmannedVehicleCommand) { + DataSourceConfig config; + config.position.host = "localhost"; + config.position.port = 8081; + config.position.aircraft_path = "/openApi/getCurrentFlightPositions"; + config.position.vehicle_path = "/openApi/getCurrentVehiclePositions"; + config.position.auth.auth_required = true; + config.position.auth.auth_path = "/login"; + config.position.auth.username = "dianxin"; + config.position.auth.password = "dianxin@123"; + config.position.timeout_ms = 5000; + config.position.read_timeout_ms = 2000; + config.position.refresh_interval_ms = 100; + + config.vehicle.host = "localhost"; + config.vehicle.port = 8081; + config.vehicle.command_path = "/api/VehicleCommandInfo"; + config.vehicle.auth.auth_required = true; + config.vehicle.auth.auth_path = "/login"; + config.vehicle.auth.username = "dianxin"; + config.vehicle.auth.password = "dianxin@123"; + config.vehicle.timeout_ms = 5000; + config.vehicle.read_timeout_ms = 2000; + + HTTPDataSource source(config); + ASSERT_TRUE(source.connect()) << "连接失败"; + + // 先获取车辆状态来初始化车辆 + std::vector vehicles; + ASSERT_TRUE(source.fetchPositionVehicleData(vehicles)) << "获取车辆状态失败"; + ASSERT_FALSE(vehicles.empty()) << "车辆数据为空"; + + // 先发送 RESUME 指令清除所有状态 + VehicleCommand resume_command; + resume_command.vehicleId = "QN001"; + resume_command.type = CommandType::RESUME; + resume_command.reason = CommandReason::RESUME_TRAFFIC; + resume_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(resume_command.vehicleId, resume_command)) << "发送恢复指令失败"; + + // 等待 RESUME 指令生效 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 发送 WARNING 指令(优先级 3) + VehicleCommand warning_command; + warning_command.vehicleId = "QN001"; + warning_command.type = CommandType::WARNING; + warning_command.reason = CommandReason::AIRCRAFT_CROSSING; + warning_command.relativeSpeed = 50; + warning_command.relativeMotionX = 50; + warning_command.relativeMotionY = 50; + warning_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(warning_command.vehicleId, warning_command)) << "发送预警指令失败"; + + // 等待 WARNING 指令生效 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 发送 ALERT 指令(优先级 5) + VehicleCommand alert_command; + alert_command.vehicleId = "QN001"; + alert_command.type = CommandType::ALERT; + alert_command.reason = CommandReason::AIRCRAFT_CROSSING; + alert_command.relativeSpeed = 100; + alert_command.relativeMotionX = 100; + alert_command.relativeMotionY = 100; + alert_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(alert_command.vehicleId, alert_command)) << "发送告警指令失败"; + + // 等待 ALERT 指令生效 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 最后发送 RESUME 指令清除状态 + resume_command.timestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + ASSERT_TRUE(source.sendUnmannedVehicleCommand(resume_command.vehicleId, resume_command)) << "发送最终恢复指令失败"; +} + +TEST_F(HTTPDataSourceTest, TrafficLightAndUnmannedAuthFailure) { + DataSourceConfig config; + config.traffic_light.host = "localhost"; + config.traffic_light.port = 8081; + config.traffic_light.signal_path = "/api/VehicleCommandInfo"; + config.traffic_light.auth.auth_required = true; + config.traffic_light.auth.username = "wrong_user"; + config.traffic_light.auth.password = "wrong_pass"; + + config.vehicle.host = "localhost"; + config.vehicle.port = 8081; + config.vehicle.command_path = "/api/VehicleCommandInfo"; + config.vehicle.auth.auth_required = true; + config.vehicle.auth.username = "wrong_user"; + config.vehicle.auth.password = "wrong_pass"; + + HTTPDataSource source(config); + + std::vector signals; + EXPECT_FALSE(source.fetchTrafficLightSignals(signals)); + + VehicleCommand command; + command.vehicleId = "TEST_VEHICLE_001"; + command.type = CommandType::SIGNAL; + command.reason = CommandReason::RESUME_TRAFFIC; + command.signalState = SignalState::GREEN; + command.timestamp = 0; + EXPECT_FALSE(source.sendUnmannedVehicleCommand(command.vehicleId, command)); +} \ No newline at end of file diff --git a/tests/SafetyZoneTest.cpp b/tests/SafetyZoneTest.cpp new file mode 100644 index 0000000..9cfbcfb --- /dev/null +++ b/tests/SafetyZoneTest.cpp @@ -0,0 +1,190 @@ +#include +#include "detector/SafetyZone.h" +#include "utils/Logger.h" +#include "config/SystemConfig.h" + +class SafetyZoneTest : public ::testing::Test { +protected: + void SetUp() override { + // 确保 SystemConfig 已初始化 + SystemConfig::instance(); + // 创建一个安全区,中心点在(0,0),飞机安全区半径50米,特勤车安全区半径30米 + safetyZone = std::make_unique(Vector2D{0, 0}, 50.0, 30.0); + } + + std::unique_ptr safetyZone; + + // 创建测试用的移动对象 + Aircraft createAircraft(const std::string& id, double x, double y) { + Aircraft obj; + obj.id = id; + obj.position = Vector2D{x, y}; + obj.geo.latitude = 0; // 这里的经纬度不重要 + obj.geo.longitude = 0; + obj.type = MovingObjectType::AIRCRAFT; // 设置类型为航空器 + return obj; + } + + Vehicle createSpecialVehicle(const std::string& id, double x, double y) { + Vehicle obj; + obj.id = id; + obj.position = Vector2D{x, y}; + obj.geo.latitude = 0; + obj.geo.longitude = 0; + obj.type = MovingObjectType::SPECIAL; // 设置类型为特勤车 + obj.isControllable = false; // 设置为非可控车辆(即特勤车) + return obj; + } + + Vehicle createUnmannedVehicle(const std::string& id, double x, double y) { + Vehicle obj; + obj.id = id; + obj.position = Vector2D{x, y}; + obj.geo.latitude = 0; + obj.geo.longitude = 0; + obj.type = MovingObjectType::UNMANNED; // 设置类型为无人车 + obj.isControllable = true; // 设置为可控车辆(即无人车) + return obj; + } + + // 获取飞机有效距离(安全区半径 + 飞机半长度) + double getAircraftEffectiveDistance() const { + const auto& config = SystemConfig::instance().collision_detection.prediction; + return safetyZone->getAircraftRadius() + config.aircraft_size / 2.0; + } + + // 获取特勤车有效距离(安全区半径 + 车辆半长度) + double getVehicleEffectiveDistance() const { + const auto& config = SystemConfig::instance().collision_detection.prediction; + return safetyZone->getSpecialVehicleRadius() + config.vehicle_size / 2.0; + } +}; + +// 测试初始状态 +TEST_F(SafetyZoneTest, InitialState) { + EXPECT_EQ(safetyZone->getState(), SafetyZoneState::INACTIVE); + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::NONE); + EXPECT_EQ(safetyZone->getCurrentRadius(), 0.0); +} + +// 测试飞机进入安全区 +TEST_F(SafetyZoneTest, AircraftEntering) { + double effectiveDistance = getAircraftEffectiveDistance(); + + // 创建一个飞机,位置在安全区边缘内侧 + auto aircraft = createAircraft("AC001", effectiveDistance - 1.0, 0.0); + + // 检查飞机是否在安全区内 + EXPECT_TRUE(safetyZone->isObjectInZone(aircraft)); + + // 尝试激活安全区 + EXPECT_TRUE(safetyZone->tryActivate(aircraft)); + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::AIRCRAFT); + EXPECT_EQ(safetyZone->getCurrentRadius(), 50.0); + + // 测试飞机完全离开安全区 + aircraft = createAircraft("AC001", effectiveDistance + 1.0, 0.0); + EXPECT_FALSE(safetyZone->isObjectInZone(aircraft)); +} + +// 测试特勤车进入安全区 +TEST_F(SafetyZoneTest, SpecialVehicleEntering) { + double effectiveDistance = getVehicleEffectiveDistance(); + + // 创建一个特勤车,位置在安全区边缘内侧 + auto vehicle = createSpecialVehicle("TQ001", effectiveDistance - 1.0, 0.0); + + // 检查特勤车是否在安全区内 + EXPECT_TRUE(safetyZone->isObjectInZone(vehicle)); + + // 尝试激活安全区 + EXPECT_TRUE(safetyZone->tryActivate(vehicle)); + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::VEHICLE); + EXPECT_EQ(safetyZone->getCurrentRadius(), 30.0); + + // 测试特勤车完全离开安全区 + vehicle = createSpecialVehicle("TQ001", effectiveDistance + 1.0, 0.0); + EXPECT_FALSE(safetyZone->isObjectInZone(vehicle)); +} + +// 测试无人车进入安全区 +TEST_F(SafetyZoneTest, UnmannedVehicleEntering) { + // 创建一个无人车,位置在安全区内 + auto vehicle = createUnmannedVehicle("UV001", 10.0, 0.0); + + // 检查无人车是否在安全区内(应该返回false,因为无人车不能设置安全区类型) + EXPECT_TRUE(safetyZone->isObjectInZone(vehicle)); + EXPECT_FALSE(safetyZone->tryActivate(vehicle)); + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::NONE); + EXPECT_EQ(safetyZone->getCurrentRadius(), 0.0); +} + +// 测试安全区类型锁定 +TEST_F(SafetyZoneTest, SafetyZoneTypeLocking) { + double aircraftEffectiveDistance = getAircraftEffectiveDistance(); + double vehicleEffectiveDistance = getVehicleEffectiveDistance(); + + // 先让飞机进入安全区 + auto aircraft = createAircraft("AC001", aircraftEffectiveDistance - 5.0, 0.0); + EXPECT_TRUE(safetyZone->isObjectInZone(aircraft)); + EXPECT_TRUE(safetyZone->tryActivate(aircraft)); + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::AIRCRAFT); + + // 尝试让特勤车进入已经被飞机设置类型的安全区 + auto vehicle = createSpecialVehicle("TQ001", vehicleEffectiveDistance - 5.0, 0.0); + EXPECT_TRUE(safetyZone->isObjectInZone(vehicle)); + EXPECT_FALSE(safetyZone->tryActivate(vehicle)); + + // 确认安全区类型和尺寸没有改变 + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::AIRCRAFT); + EXPECT_EQ(safetyZone->getCurrentRadius(), 50.0); +} + +// 测试边界条件 +TEST_F(SafetyZoneTest, BoundaryConditions) { + double aircraftEffectiveDistance = getAircraftEffectiveDistance(); + double vehicleEffectiveDistance = getVehicleEffectiveDistance(); + + // 测试飞机在边界上 + auto aircraft1 = createAircraft("AC001", aircraftEffectiveDistance, 0.0); + EXPECT_TRUE(safetyZone->isObjectInZone(aircraft1)); + EXPECT_TRUE(safetyZone->tryActivate(aircraft1)); + + // 测试飞机在边界外 + auto aircraft2 = createAircraft("AC002", aircraftEffectiveDistance + 0.1, 0.0); + EXPECT_FALSE(safetyZone->isObjectInZone(aircraft2)); + + // 重新创建安全区用于测试特勤车 + safetyZone = std::make_unique(Vector2D{0, 0}, 50.0, 30.0); + + // 测试特勤车在边界上 + auto vehicle1 = createSpecialVehicle("TQ001", vehicleEffectiveDistance, 0.0); + EXPECT_TRUE(safetyZone->isObjectInZone(vehicle1)); + EXPECT_TRUE(safetyZone->tryActivate(vehicle1)); + + // 测试特勤车在边界外 + auto vehicle2 = createSpecialVehicle("TQ002", vehicleEffectiveDistance + 0.1, 0.0); + EXPECT_FALSE(safetyZone->isObjectInZone(vehicle2)); +} + +// 测试状态重置 +TEST_F(SafetyZoneTest, StateReset) { + double effectiveDistance = getAircraftEffectiveDistance(); + + // 先让飞机进入安全区 + auto aircraft = createAircraft("AC001", effectiveDistance - 5.0, 0.0); + EXPECT_TRUE(safetyZone->isObjectInZone(aircraft)); + EXPECT_TRUE(safetyZone->tryActivate(aircraft)); + + // 设置状态为激活 + safetyZone->setState(SafetyZoneState::ACTIVE); + EXPECT_EQ(safetyZone->getState(), SafetyZoneState::ACTIVE); + + // 重置状态 + safetyZone->setState(SafetyZoneState::INACTIVE); + EXPECT_EQ(safetyZone->getState(), SafetyZoneState::INACTIVE); + + // 确认类型和尺寸保持不变 + EXPECT_EQ(safetyZone->getType(), SafetyZoneType::AIRCRAFT); + EXPECT_EQ(safetyZone->getCurrentRadius(), 50.0); +} \ No newline at end of file diff --git a/tests/TrafficLightDetectorTest.cpp b/tests/TrafficLightDetectorTest.cpp new file mode 100644 index 0000000..7a6561b --- /dev/null +++ b/tests/TrafficLightDetectorTest.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include "detector/TrafficLightDetector.h" +#include "config/IntersectionConfig.h" +#include "vehicle/ControllableVehicles.h" +#include "utils/Logger.h" +#include "types/VehicleCommand.h" +#include "types/TrafficLightTypes.h" +#include "core/System.h" + +// Mock System 类 +class MockSystem : public System { +public: + MOCK_METHOD(void, broadcastTrafficLightCommand, (const std::string&, const VehicleCommand&)); + static MockSystem& getInstance() { + static MockSystem instance; + return instance; + } +private: + MockSystem() = default; + MockSystem(const MockSystem&) = delete; + MockSystem& operator=(const MockSystem&) = delete; +}; + +class TrafficLightDetectorTest : public ::testing::Test { +protected: + void SetUp() override { + // 创建测试用的交叉口配置 + Intersection intersection; + intersection.id = "INT001"; + intersection.name = "Test Intersection"; + intersection.position.latitude = 36.36; + intersection.position.longitude = 120.08; + intersection.position.altitude = 9.5; + intersection.width = 50.0; + intersection.trafficLightId = "TL001"; + + // 创建临时配置文件 + std::ofstream config_file("test_intersections.json"); + nlohmann::json j; + j["intersections"] = nlohmann::json::array(); + j["intersections"].push_back({ + {"id", intersection.id}, + {"name", intersection.name}, + {"position", { + {"latitude", intersection.position.latitude}, + {"longitude", intersection.position.longitude}, + {"altitude", intersection.position.altitude} + }}, + {"width", intersection.width}, + {"trafficLightId", intersection.trafficLightId}, + {"safetyZone", { + {"aircraftRadius", 50.0}, + {"vehicleRadius", 50.0} + }} + }); + config_file << j.dump(4); + config_file.close(); + + intersectionConfig = IntersectionConfig::load("test_intersections.json"); + detector = std::make_unique( + intersectionConfig, + ControllableVehicles::getInstance(), + MockSystem::getInstance() + ); + } + + void TearDown() override { + detector.reset(); + std::remove("test_intersections.json"); + } + + // 创建测试用的红绿灯信号 + TrafficLightSignal createTestSignal(const std::string& id, SignalStatus status) { + TrafficLightSignal signal; + signal.trafficLightId = id; + signal.ns_status = status; + signal.ew_status = status; + signal.timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + return signal; + } + + // 创建测试用的车辆数据 + Vehicle createTestVehicle(const std::string& id, double lat, double lon) { + Vehicle v; + v.id = id; + v.vehicleNo = id; + v.geo.latitude = lat; + v.geo.longitude = lon; + v.timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + return v; + } + + std::unique_ptr detector; + IntersectionConfig intersectionConfig; +}; + +// 测试红绿灯信号处理 +TEST_F(TrafficLightDetectorTest, SignalProcessing) { + // 创建一个在交叉口范围内的车辆 + Vehicle vehicle = createTestVehicle("V001", 36.36, 120.08); + std::vector vehicles = {vehicle}; + + // 测试红灯 + TrafficLightSignal redSignal = createTestSignal("TL001", SignalStatus::RED); + + detector->processSignal(redSignal, vehicles); + + // 测试绿灯 + TrafficLightSignal greenSignal = createTestSignal("TL001", SignalStatus::GREEN); + + detector->processSignal(greenSignal, vehicles); +} + +// 测试无效的红绿灯信号 +TEST_F(TrafficLightDetectorTest, InvalidSignal) { + Vehicle vehicle = createTestVehicle("V001", 36.36, 120.08); + std::vector vehicles = {vehicle}; + + // 测试无效的红绿灯ID + TrafficLightSignal invalidSignal = createTestSignal("TL999", SignalStatus::RED); + + detector->processSignal(invalidSignal, vehicles); +} + +// 测试边界情况 +TEST_F(TrafficLightDetectorTest, EdgeCases) { + // 测试空车辆列表 + std::vector emptyVehicles; + TrafficLightSignal signal = createTestSignal("TL001", SignalStatus::RED); + + detector->processSignal(signal, emptyVehicles); + + // 测试交叉口范围外的车辆 + Vehicle farVehicle = createTestVehicle("V002", 36.40, 120.12); + std::vector vehicles = {farVehicle}; + + detector->processSignal(signal, vehicles); +} \ No newline at end of file diff --git a/tests/data/invalid_airport_bounds.json b/tests/data/invalid_airport_bounds.json new file mode 100644 index 0000000..bb7f5b4 --- /dev/null +++ b/tests/data/invalid_airport_bounds.json @@ -0,0 +1,5 @@ +{ + "airport": { + "rotation_angle": 68.53 + } +} \ No newline at end of file diff --git a/tools/map.html b/tools/map.html new file mode 100644 index 0000000..7c2dbd7 --- /dev/null +++ b/tools/map.html @@ -0,0 +1,303 @@ + + + + + 机场路口地图 + + + +

+ + + + \ No newline at end of file diff --git a/tools/map_websocket.html b/tools/map_websocket.html new file mode 100644 index 0000000..3a2dbba --- /dev/null +++ b/tools/map_websocket.html @@ -0,0 +1,740 @@ + + + + + 机场车辆监控 + + + + + + + +

机场车辆监控系统

+
+
+
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/tools/mock_server.py b/tools/mock_server.py new file mode 100644 index 0000000..853d8a3 --- /dev/null +++ b/tools/mock_server.py @@ -0,0 +1,1162 @@ +from flask import Flask, jsonify, request +import time +import math +import random +import logging +import os +import uuid + +# 创建 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__) + +# 地球半径(米) +EARTH_RADIUS = 6378137.0 + +COMMAND_PRIORITIES = { + "ALERT": 5, + "WARNING": 3, + "PARKING": 2, + "RESUME": 1 +} + +def meters_to_degrees(meters, latitude): + """ + 将米转换为度,考虑纬度的影响 + """ + # 纬度方向:1度 = 111,319.9米 + meters_per_deg_lat = 111319.9 + # 经度方向:1度 = 111,319.9 * cos(latitude)米 + meters_per_deg_lon = 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 = 10.0 # 返回起点后的等待时间 +UPDATE_INTERVAL = 1.0 # 位置更新间隔, 默认 1 秒 +TRAFFIC_LIGHT_SWITCH_INTERVAL = 10.0 # 红绿灯切换间隔 + +# 两个路口的位置 +T2_INTERSECTION = {"longitude": 120.08502054, "latitude": 36.35448347} +T6_INTERSECTION = {"longitude": 120.08649105, "latitude": 36.35074527} + +# 坐标点 +# 无人车1起点 +POINT_T1 = {"longitude": 120.0868853, "latitude": 36.35496367} +# 冲突点 1 +POINT_T2 = {"longitude": 120.08502054, "latitude": 36.35448347} +# 特勤车终点 +POINT_T3 = {"longitude": 120.08341044, "latitude": 36.35406879} +# 无人车1终点, 无人车 2 起点,特勤车起点 +POINT_T4 = {"longitude": 120.08558121, "latitude": 36.35305878} +POINT_T5 = {"longitude": 120.08400957, "latitude": 36.35265197} +# 冲突点 2 +POINT_T6 = {"longitude": 120.08649105, "latitude": 36.35074527} +# 航空器 1终点 +POINT_T7 = {"longitude": 120.08562915, "latitude": 36.35052372} +# 无人车 2终点 +POINT_T8 = {"longitude": 120.08676664, "latitude": 36.35004529} +POINT_T9 = {"longitude": 120.08520616, "latitude": 36.34964473} +POINT_T10 = {"longitude": 120.08710569, "latitude": 36.34917893} +# 航空器 1终点 +POINT_T11 = {"longitude": 120.0873865, "latitude": 36.3509885} + +# QN002 新起点(距离 T6 路口 150 米) +POINT_T12 = {"longitude": 120.08603613, "latitude": 36.35190217} # T4 和 T6 之间,距 T6 150米 + +# AC001 新起点(距离 T6 路口 200 米) +POINT_T13 = {"longitude": 120.08509148, "latitude": 36.35041247} # T6向南 200 米 + +# 飞机和车辆尺寸(半径 米) +AIRCRAFT_SIZE_M = 30.0 +VEHICLE_SIZE_M = 10.0 + +# 计算初始方向向量 +initial_target_lat = POINT_T11["latitude"] - POINT_T7["latitude"] +initial_target_lon = POINT_T11["longitude"] - POINT_T7["longitude"] + +# 归一化方向向量 +initial_dist = math.sqrt(initial_target_lat * initial_target_lat + initial_target_lon * initial_target_lon) +if initial_dist > 0: + initial_target_lat /= initial_dist + initial_target_lon /= initial_dist + +# 修改航空器数据的初始位置和方向 +aircraft_data = [ + { + "flightNo": "AC001", # 航空器 + "longitude": POINT_T7["longitude"], + "latitude": POINT_T7["latitude"], + "time": int(time.time() * 1000), + "altitude": 0.0, + "trackNumber": 1001, # 改为数字格式 + "speed": 36.0, # 内部使用的速度字段,不会在 API 返回中显示 + "direction": { # 内部使用的方向字段,不会在 API 返回中显示 + "lat": initial_target_lat, + "lon": initial_target_lon + } + } +] + +# 添加默认速度常量 +DEFAULT_VEHICLE_SPEED = 18.0 # km/h,默认 36km/h +EMERGENCY_BRAKE_DECELERATION = 0.8 # 紧急制动减速度 (每次更新减速 80%) +NORMAL_BRAKE_DECELERATION = 0.2 # 正常制动减速度 (每次更新减速 20%) + +# 计算 QN002 的初始方向向量 +qn002_target_lat = POINT_T8["latitude"] - POINT_T4["latitude"] +qn002_target_lon = POINT_T8["longitude"] - POINT_T4["longitude"] + +# 归一化方向向量 +qn002_dist = math.sqrt(qn002_target_lat * qn002_target_lat + qn002_target_lon * qn002_target_lon) +if qn002_dist > 0: + qn002_target_lat /= qn002_dist + qn002_target_lon /= qn002_dist + +# 修改车辆数据中 QN002 的初始位置和方向 +vehicle_data = [ + { + "vehicleNo": "QN001", # 无人车1 + "longitude": POINT_T1["longitude"], + "latitude": POINT_T1["latitude"], + "time": int(time.time() * 1000), + "direction": 180.0, # 向南方向为180度 + "speed": DEFAULT_VEHICLE_SPEED + }, + { + "vehicleNo": "QN002", # 无人车2 + "longitude": POINT_T12["longitude"], + "latitude": POINT_T12["latitude"], + "time": int(time.time() * 1000), + "direction": 90.0, # 向东方向为90度 + "speed": DEFAULT_VEHICLE_SPEED + }, + { + "vehicleNo": "TQ001", # 特勤车 + "longitude": POINT_T4["longitude"], + "latitude": POINT_T4["latitude"], + "time": int(time.time() * 1000), + "direction": 270.0, # 向西方向为270度 + "speed": DEFAULT_VEHICLE_SPEED + } +] + +# 添加车辆状态类 +class VehicleState: + def __init__(self, vehicle_no): + self.vehicle_no = vehicle_no + self.is_running = True # 运行状态 + self.current_command = None # 当前执行的指令 + self.command_priority = 0 # 当前指令优先级 + self.target_speed = DEFAULT_VEHICLE_SPEED # 目标速度 + self.brake_mode = None # 制动模式:'emergency' 或 'normal' + self.command_reason = None # 指令原因 + self.last_command_time = time.time() # 最后一次指令时间 + self.traffic_light_state = None # 当前红绿灯状态 + self.target_lat = None # 目标纬度 + self.target_lon = None # 目标经度 + self.is_traffic_light_stop = False # 是否因红灯停车 + + def can_be_overridden_by(self, command_type): + """判断当前指令是否可以被新指令覆盖""" + # 红绿灯信号不参与指令优先级判断 + if command_type in ["RED", "GREEN", "YELLOW"]: + return True + + new_priority = COMMAND_PRIORITIES.get(command_type, 0) + current_priority = COMMAND_PRIORITIES.get(self.current_command, 0) + + # 相同类型的指令可以覆盖(因为需要持续发送) + if 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, target_lat=None, target_lon=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): + """检查车辆是否可以移动""" + # 如果有告警指令,不能移动 + 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): + """记录当前车辆状态""" + 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[vehicle["vehicleNo"]] = VehicleState(vehicle["vehicleNo"]) + +def calculate_distance_to_target(vehicle, target_lat, target_lon): + """计算车辆到目标位的距离(米)""" + dlat = target_lat - vehicle["latitude"] + dlon = target_lon - vehicle["longitude"] + + # 使用 Haversine 公式计算距离 + R = 6371000 # 地球半径(米) + a = math.sin(dlat/2) * math.sin(dlat/2) + \ + math.cos(math.radians(vehicle["latitude"])) * math.cos(math.radians(target_lat)) * \ + math.sin(dlon/2) * math.sin(dlon/2) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) + return R * c + +@app.route('/api/VehicleCommandInfo', methods=['POST']) +def handle_vehicle_command(): + try: + data = request.json + 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() + + # 特勤车只响应红绿灯信号 + if vehicle_id.startswith("TQ"): + 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={vehicle_state.current_command}, " + f"new_command={check_command}, can_override={can_override}") + + if not can_override: + print(f"指令优先级过低: vehicle={vehicle_id}, current_priority={vehicle_state.command_priority}, " + f"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 + # 立即更新车辆速度 + 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 calculate_distance_to_intersection(vehicle, intersection): + """计算车辆到路口的距离(米)""" + vehicle_lat = vehicle["latitude"] + vehicle_lon = vehicle["longitude"] + intersection_lat = intersection["latitude"] + intersection_lon = intersection["longitude"] + + # 使用经纬度计算距离 + lat_diff = (vehicle_lat - intersection_lat) * 111319.9 + lon_diff = (vehicle_lon - intersection_lon) * (111319.9 * math.cos(math.radians(vehicle_lat))) + return math.sqrt(lat_diff * lat_diff + lon_diff * lon_diff) + +def calculate_path_direction(start_point, end_point): + """计算两点之间的方向角度(相对于正北方向,顺时针)""" + dlat = end_point["latitude"] - start_point["latitude"] + dlon = end_point["longitude"] - start_point["longitude"] + angle_rad = math.atan2(dlon, dlat) # 使用 atan2 计算角度 + angle_deg = math.degrees(angle_rad) + # 转换为顺时针方向(正北为0度) + return (90 - angle_deg) % 360 + +def get_front_traffic_light(vehicle, distance_to_west, distance_to_east): + """获取车辆前方的红绿灯状态""" + # 根据车辆的位置和方向判断前方路口 + if vehicle["vehicleNo"] == "QN001": + # 确保 phase 属性存在 + if "phase" not in vehicle: + vehicle["phase"] = 0 + + # QN001 的路线:T1 -> T2 -> T4 + if vehicle["phase"] == 0: # T1 -> T2 阶段 + # 计算 T1 到 T2 的实际方向 + actual_direction = calculate_path_direction(POINT_T1, POINT_T2) + # 允许 15 度的误差范围 + direction_diff = abs(vehicle["direction"] - actual_direction) + direction_diff = min(direction_diff, 360 - direction_diff) + + # 在西路口以北时,向 T2 移动需要判断西路口红绿灯 + if direction_diff <= 15 and vehicle["latitude"] > T2_INTERSECTION["latitude"]: + return traffic_light_data[0], distance_to_west # 西路口红绿灯 + + elif vehicle["vehicleNo"] == "QN002": + # 计算 T12 到 T8 的实际方向 + actual_direction = calculate_path_direction(POINT_T12, POINT_T8) + # 允许 15 度的误差范围 + direction_diff = abs(vehicle["direction"] - actual_direction) + direction_diff = min(direction_diff, 360 - direction_diff) + + # 在东路口以西时,向 T8 移动需要判断东路红绿灯 + if direction_diff <= 15 and vehicle["longitude"] < T6_INTERSECTION["longitude"]: + return traffic_light_data[1], distance_to_east # 东路口红绿灯 + + elif vehicle["vehicleNo"] == "TQ001": + # 确保 phase 属性存在 + if "phase" not in vehicle: + vehicle["phase"] = 0 + + # TQ001 的路线:T4 -> T2 -> T3 + if vehicle["phase"] == 0: # T4 -> T2 阶段 + # 计算 T4 到 T2 的实际方向 + actual_direction = calculate_path_direction(POINT_T4, POINT_T2) + # 允许 15 度的误差范围 + direction_diff = abs(vehicle["direction"] - actual_direction) + direction_diff = min(direction_diff, 360 - direction_diff) + + # 在西路口以东时,向 T2 移动需要判断西路口红绿灯 + if direction_diff <= 15 and vehicle["longitude"] > T2_INTERSECTION["longitude"]: + return traffic_light_data[0], distance_to_west # 西路口红绿灯 + + # 其他情况,表示车辆已经过路口或不需要判断红绿灯 + return None, float('inf') + +def update_position_with_vector(obj, target_point, start_point, speed, elapsed_time, return_to_start=False): + """ + 基于向量的位置更新逻辑 + + 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 + + current_time = time.time() + if current_time < obj["wait_until"]: + return False # 还在等待中,不更新位置 + + # 计算这一步要移动的距离(米)并转换为经纬度 + speed_mps = speed * 1000 / 3600 + distance = speed_mps * elapsed_time + + # 将移动距离转换为经纬度变化量 + move_dlat, move_dlon = meters_to_degrees(distance, obj["latitude"]) + + # 将15米的判断距离转换为经纬度 + check_dlat, check_dlon = meters_to_degrees(15, obj["latitude"]) + + # 计算当前位置到终点的向量 + vector_lat = target_point["latitude"] - obj["latitude"] + vector_lon = target_point["longitude"] - obj["longitude"] + + # 计算向量长度(经纬度空间) + 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): + if return_to_start: + # 返回起点并设置等待时间 + obj["latitude"] = start_point["latitude"] + obj["longitude"] = start_point["longitude"] + obj["wait_until"] = current_time + WAIT_TIME_AFTER_RETURN # 使用常量 + print(f"对象 {obj.get('flightNo', obj.get('vehicleNo'))} 返回起点,等待{WAIT_TIME_AFTER_RETURN}秒后继续") + else: + # 否则就停在目标点 + obj["latitude"] = target_point["latitude"] + obj["longitude"] = target_point["longitude"] + 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 + + # 更新位置 + obj["latitude"] += dlat + obj["longitude"] += dlon + return False # 表示正在移动中 + +def update_aircraft_position(aircraft, elapsed_time): + """更新航空器位置""" + reached_target = update_position_with_vector( + aircraft, + POINT_T11, # 目标点 + POINT_T13, # 起点 + aircraft["speed"], + elapsed_time, + return_to_start=True # 航空器需要返回起点 + ) + + if reached_target: + print(f"航空器 {aircraft['flightNo']} 到达终点,返回起点 T7") + +def update_vehicle_position(vehicle, elapsed_time): + """更新车辆位置""" + # 获取车辆状态 + vehicle_state = vehicle_states.get(vehicle["vehicleNo"]) + if not vehicle_state: + return + + # 确保所有车辆都有 phase 属性 + if "phase" not in vehicle: + vehicle["phase"] = 0 + print(f"初始化车辆 {vehicle['vehicleNo']} 的 phase 属性为 0") + + # 获取前方红绿灯状态和距离 + traffic_light, distance = get_front_traffic_light(vehicle, + calculate_distance_to_intersection(vehicle, T2_INTERSECTION), + calculate_distance_to_intersection(vehicle, T6_INTERSECTION)) + + # 特勤车只响应红绿灯 + if vehicle["vehicleNo"].startswith("TQ"): + # 红灯且距离停止线小于等于 50 米时停车 + if traffic_light and traffic_light["state"] == 0 and distance <= DIST_50M: + vehicle["speed"] = 0 + print(f"特勤车 {vehicle['vehicleNo']} 遇红灯在停止线前停车,距路口 {distance:.1f}米") + return + # 其他情况正常行驶 + vehicle["speed"] = DEFAULT_VEHICLE_SPEED + print(f"特勤车 {vehicle['vehicleNo']} 正常行驶,速度={vehicle['speed']}km/h") + else: + # 普通车辆的移动逻辑 + # 红灯且距离停止线小于等于 50 米时必须停车 + if traffic_light and traffic_light["state"] == 0 and distance <= DIST_50M: + vehicle["speed"] = 0 + vehicle_state.current_command = "RED" # 设置当前指令为红灯 + vehicle_state.is_running = False # 设置为停止状态 + print(f"车辆 {vehicle['vehicleNo']} 遇红灯在停止线前停车,距路口 {distance:.1f}米") + return + elif vehicle_state.current_command in ["ALERT", "WARNING"]: + # 其他停车指令 + vehicle["speed"] = 0 + print(f"车辆 {vehicle['vehicleNo']} 不能移动: command={vehicle_state.current_command}") + return + + # 可以移动,设置正常速度 + vehicle["speed"] = DEFAULT_VEHICLE_SPEED + print(f"车辆 {vehicle['vehicleNo']} 可以移动,速度={vehicle['speed']}km/h") + + # 更新位置 + if vehicle["vehicleNo"].startswith("QN"): + # 无人车的路径更新逻辑 + if vehicle["vehicleNo"] == "QN001": + if vehicle["phase"] == 0: # T1 -> T2 + reached_target = update_position_with_vector( + vehicle, + POINT_T2, # 目标点(西路口) + POINT_T1, # 起点 + vehicle["speed"], + elapsed_time, + return_to_start=False # 不返回起点,继续到下一个点 + ) + + if reached_target: + vehicle["phase"] = 1 # 切换到下一阶段 + print(f"无人车 {vehicle['vehicleNo']} 到达 T2,开始前往 T4") + + elif vehicle["phase"] == 1: # T2 -> T4 + reached_target = update_position_with_vector( + vehicle, + POINT_T4, # 目标点 + POINT_T2, # 起点 + vehicle["speed"], + elapsed_time, + return_to_start=False # 不返回起点,继续到下一个点 + ) + + if reached_target: + # 到达 T4 后直接跳回起点 T1 + vehicle["latitude"] = POINT_T1["latitude"] + vehicle["longitude"] = POINT_T1["longitude"] + vehicle["phase"] = 0 # 重置到初始阶段 + print(f"无人车 {vehicle['vehicleNo']} 到达 T4,直接返回起点 T1") + + else: # QN002 + reached_target = update_position_with_vector( + vehicle, + POINT_T8, # 目标点 + POINT_T12, # 使用新的起点 + vehicle["speed"], + elapsed_time, + return_to_start=True # QN002 需要返回起点 + ) + + if reached_target: + print(f"无人车 {vehicle['vehicleNo']} 到达终点,返回起点 T12") + + elif vehicle["vehicleNo"].startswith("TQ"): + # 特勤车的路径更新逻辑 + if vehicle["phase"] == 0: # T4 -> T2 + reached_target = update_position_with_vector( + vehicle, + POINT_T2, # 目标点(西路口) + POINT_T4, # 起点 + vehicle["speed"], + elapsed_time, + return_to_start=False # 不返回起点,继续到下一个点 + ) + + if reached_target: + vehicle["phase"] = 1 # 切换到下一阶段 + print(f"特勤车 {vehicle['vehicleNo']} 到达 T2,开始前往 T3") + + elif vehicle["phase"] == 1: # T2 -> T3 + reached_target = update_position_with_vector( + vehicle, + POINT_T3, # 目标点(特勤车终点) + POINT_T2, # 起点 + vehicle["speed"], + elapsed_time, + return_to_start=False # 不返回起点,继续到下一个点 + ) + + if reached_target: + # 到达 T3 后直接跳回起点 T4 + vehicle["latitude"] = POINT_T4["latitude"] + vehicle["longitude"] = POINT_T4["longitude"] + vehicle["phase"] = 0 # 重置到初始阶段 + print(f"特勤车 {vehicle['vehicleNo']} 到达 T3,直接返回起点 T4") + + # 新时间戳 + vehicle["time"] = int(time.time() * 1000) + +# 定义路口信息 +INTERSECTIONS = { + "TL001": { + "id": "INT001", + "name": "西路口", + "latitude": T2_INTERSECTION["latitude"], + "longitude": T2_INTERSECTION["longitude"] + }, + "TL002": { + "id": "INT002", + "name": "东路口", + "latitude": T6_INTERSECTION["latitude"], + "longitude": T6_INTERSECTION["longitude"] + } +} + +def generate_traffic_light_data(): + """生成红绿灯数据""" + 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_light_switch_time = time.time() + +def check_auth(): + auth_header = request.headers.get('Authorization') + if not auth_header or auth_header != AUTH_TOKEN: + return False + return True + +@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: + api_aircraft = { + "flightNo": aircraft["flightNo"], + "longitude": aircraft["longitude"], + "latitude": aircraft["latitude"], + "time": aircraft["time"], + "altitude": aircraft["altitude"], + "trackNumber": aircraft["trackNumber"] + } + response_data.append(api_aircraft) + + return jsonify({ + "status": 200, + "msg": "当前航空器实时位置数据", + "data": response_data + }) + +def switch_traffic_light_state(): + """统一处理红绿灯状态切换""" + 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] + lat_diff = abs(aircraft["latitude"] - T6_INTERSECTION["latitude"]) * 111319.9 + + old_state = traffic_light_data[1]["state"] + traffic_light_data[1]["state"] = 1 if lat_diff > DIST_50M else 1 + + 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: + # 统一处理红绿灯状态切换 + # switch_traffic_light_state() # 采用模拟的红绿灯脚本,注释掉此处的调用 + + # 只在达到更新间隔时更新位置 + if elapsed_time >= UPDATE_INTERVAL: + for vehicle in vehicle_data: + update_vehicle_position(vehicle, UPDATE_INTERVAL) + vehicle["time"] = int(current_time * 1000) + last_vehicle_update_time = current_time + + return jsonify({ + "status": 200, + "msg": "当前车辆实时位置数据", + "data": vehicle_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 + + # --- 注释掉原有逻辑 (2025-05-06 by AI Assistant) --- + # 原因: 为了测试新的 TrafficLightHttpServer 推送机制,暂时禁用此轮询接口返回数据。 + # 模拟和发送红绿灯状态的功能现在由 tools/mock_traffic_light_client.py 负责。 + # 如需恢复轮询测试,请取消下面的注释并注释掉新的 return 语句。 + # # 更新红绿灯状态 + # # switch_traffic_light_state() # 采用模拟的红绿灯脚本,注释掉此处的调用 + # + # # 更新时间戳 + # for signal in traffic_light_data: + # signal[\"timestamp\"] = int(time.time() * 1000) + # + # return jsonify({ + # \"status\": 200, + # \"msg\": \"获取红绿灯信号成功\", + # \"data\": traffic_light_data + # }) + # --- 注释结束 --- + + # 新增: 返回空数据,以禁用轮询数据源 + 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 + + # 从 URL query parameters 中获取用户名和密码 + username = request.args.get('username') + password = request.args.get('password') + + print(f"收到登录请求: username={username}, password={password}") + print(f"请求 URL: {request.url}") + print(f"请求方法: {request.method}") + print(f"请求参数: {request.args}") + + 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(): + """生成无人车位置数据,符合官方API格式""" + unmanned_vehicles = [] + + # 从现有vehicle_data中筛选无人车(QN开头的车辆) + for vehicle in vehicle_data: + if vehicle["vehicleNo"].startswith("QN"): + location_info = { + "transId": str(uuid.uuid4()), + "timestamp": int(time.time() * 1000), + "vehicleID": vehicle["vehicleNo"], + "latitude": vehicle["latitude"], + "longitude": vehicle["longitude"], + "speed": vehicle["speed"] / 3.6, # 转换为m/s + "direction": math.radians(vehicle["direction"]) # 转换为弧度 + } + unmanned_vehicles.append(location_info) + + return unmanned_vehicles + +# 无人车状态数据生成函数 +def generate_unmanned_vehicle_state_data(vehicle_id=None, is_single=True): + """生成无人车状态数据,符合官方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 vehicle_data: + if vehicle["vehicleNo"].startswith("QN"): + 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_vehicle_update_time + elapsed_time = current_time - last_vehicle_update_time + + if elapsed_time >= UPDATE_INTERVAL: + for vehicle in vehicle_data: + update_vehicle_position(vehicle, UPDATE_INTERVAL) + vehicle["time"] = int(current_time * 1000) + last_vehicle_update_time = current_time + + # 生成无人车位置数据 + location_data = generate_unmanned_vehicle_location_data() + + print(f"返回无人车位置数据,数量: {len(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 + +if __name__ == '__main__': + app.run(host='localhost', port=8090, debug=True) \ No newline at end of file diff --git a/tools/mock_traffic_light_client.py b/tools/mock_traffic_light_client.py new file mode 100644 index 0000000..d6dc31a --- /dev/null +++ b/tools/mock_traffic_light_client.py @@ -0,0 +1,109 @@ +import time +import json +import urllib.request +import urllib.error +import logging +import os +import sys + +# --- 配置 --- +CPP_SERVER_URL = "http://localhost:8082/trafficlight" # C++ TrafficLightHttpServer 的地址和端口 +SEND_INTERVAL = 1.0 # 发送间隔(秒) + +# 红绿灯循环周期 (North/South State, East/West State, Duration in seconds) +TRAFFIC_LIGHT_CYCLE = [ + ("GREEN", "RED", 10), + ("YELLOW", "RED", 2), + ("RED", "GREEN", 10), + ("RED", "YELLOW", 2), +] + +# --- 日志设置 --- +LOG_DIR = 'logs' +if not os.path.exists(LOG_DIR): + try: + os.makedirs(LOG_DIR) + except OSError as e: + print(f"Error creating log directory {LOG_DIR}: {e}", file=sys.stderr) + LOG_DIR = '.' # Fallback to current directory + +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(os.path.join(LOG_DIR, 'mock_traffic_light_client.log')), + logging.StreamHandler() # 同时输出到控制台 + ] +) + +# --- 全局状态 --- +current_cycle_index = 0 +last_cycle_switch_time = time.time() + +# --- 辅助函数 --- +def generate_di_payload(ns_state, ew_state): + """根据南北和东西状态生成 DI 信号 Payload。""" + # 初始化所有 DI 信号为 0 + payload = {f"DI-{i:02d}": 0 for i in range(1, 19)} + # 设置南北向状态 (DI-11: 北红, DI-12: 北黄, DI-13: 北绿) + if ns_state == "RED": payload["DI-11"] = 1 + elif ns_state == "YELLOW": payload["DI-12"] = 1 + elif ns_state == "GREEN": payload["DI-13"] = 1 + # 设置东西向状态 (DI-14: 东红, DI-15: 东黄, DI-16: 东绿) + if ew_state == "RED": payload["DI-14"] = 1 + elif ew_state == "YELLOW": payload["DI-15"] = 1 + elif ew_state == "GREEN": payload["DI-16"] = 1 + return payload + +def send_traffic_light_update(url, payload): + """使用 urllib 发送 POST 请求。""" + try: + data = json.dumps(payload).encode('utf-8') + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}, method='POST') + # 设置一个较短的超时时间,避免长时间阻塞 + with urllib.request.urlopen(req, timeout=0.8) as response: + logging.debug(f"Sent: {payload}, Status: {response.status}") + # 可以选择性地读取响应内容 + # response.read() + except (urllib.error.URLError, TimeoutError, ConnectionRefusedError) as e: + logging.error(f"Failed to send to {url}: {e}") + except Exception as e: + logging.error(f"Unexpected error during send: {e}") + +# --- 主循环 --- +def traffic_light_sender_loop(): + """主循环,管理状态切换并发送数据。""" + global current_cycle_index, last_cycle_switch_time + logging.info(f"Starting traffic light client, sending to {CPP_SERVER_URL}") + while True: + try: + now = time.time() + + # 检查是否需要切换循环阶段 + current_ns_state, current_ew_state, duration = TRAFFIC_LIGHT_CYCLE[current_cycle_index] + if now - last_cycle_switch_time >= duration: + current_cycle_index = (current_cycle_index + 1) % len(TRAFFIC_LIGHT_CYCLE) + last_cycle_switch_time = now + next_ns_state, next_ew_state, _ = TRAFFIC_LIGHT_CYCLE[current_cycle_index] + logging.info(f"Switching phase: {current_ns_state}/{current_ew_state} -> {next_ns_state}/{next_ew_state}") + # 更新当前状态以供本次发送 + current_ns_state, current_ew_state, _ = TRAFFIC_LIGHT_CYCLE[current_cycle_index] + + # 生成 Payload 并发送 + payload = generate_di_payload(current_ns_state, current_ew_state) + send_traffic_light_update(CPP_SERVER_URL, payload) + + # 等待指定间隔 + time.sleep(SEND_INTERVAL) + + except KeyboardInterrupt: + logging.info("Traffic light client stopped by user.") + break + except Exception as e: + logging.error(f"Error in main loop: {e}", exc_info=True) + # 等待一段时间再重试,避免快速失败循环 + time.sleep(5) + +# --- 启动入口 --- +if __name__ == '__main__': + traffic_light_sender_loop() \ No newline at end of file diff --git a/tools/test_websocket.html b/tools/test_websocket.html new file mode 100644 index 0000000..908630a --- /dev/null +++ b/tools/test_websocket.html @@ -0,0 +1,135 @@ + + + + WebSocket 测试 + + + +

WebSocket 测试客户端

+
+
+ + + +
+ + + + \ No newline at end of file