增加了与后台通信的Socket模块,增加了超时告警
This commit is contained in:
parent
16048839bd
commit
b642bf3a6f
@ -30,7 +30,7 @@
|
||||
- 格式统一一致
|
||||
|
||||
请遵循如下 C++ 规范:
|
||||
- 使用 C++17 标准
|
||||
- 使用 C++20 标准
|
||||
- 使用 CMake 3.14 及以上版本
|
||||
- 使用 nlohmann_json 3.11.3 版本
|
||||
- 使用 FetchContent 管理第三方库
|
||||
|
||||
102
Assets/Scripts/WebSocketClient.cs
Normal file
102
Assets/Scripts/WebSocketClient.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using UnityEngine;
|
||||
using NativeWebSocket;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
public class WebSocketClient : MonoBehaviour
|
||||
{
|
||||
private WebSocket websocket;
|
||||
private readonly string serverUrl = "ws://localhost:8080";
|
||||
|
||||
async void Start()
|
||||
{
|
||||
websocket = new WebSocket(serverUrl);
|
||||
|
||||
websocket.OnOpen += () =>
|
||||
{
|
||||
Debug.Log("Connection open!");
|
||||
};
|
||||
|
||||
websocket.OnError += (e) =>
|
||||
{
|
||||
Debug.LogError($"Error! {e}");
|
||||
};
|
||||
|
||||
websocket.OnClose += (e) =>
|
||||
{
|
||||
Debug.Log("Connection closed!");
|
||||
};
|
||||
|
||||
websocket.OnMessage += (bytes) =>
|
||||
{
|
||||
var message = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
HandleMessage(message);
|
||||
};
|
||||
|
||||
await websocket.Connect();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
websocket.DispatchMessageQueue();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void HandleMessage(string jsonMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JObject.Parse(jsonMessage);
|
||||
string messageType = json["type"].ToString();
|
||||
|
||||
switch (messageType)
|
||||
{
|
||||
case "position_update":
|
||||
HandlePositionUpdate(json);
|
||||
break;
|
||||
case "collision_warning":
|
||||
HandleCollisionWarning(json);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"Error parsing message: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePositionUpdate(JObject json)
|
||||
{
|
||||
string objectId = json["objectId"].ToString();
|
||||
string objectType = json["objectType"].ToString();
|
||||
double longitude = json["longitude"].Value<double>();
|
||||
double latitude = json["latitude"].Value<double>();
|
||||
double heading = json["heading"].Value<double>();
|
||||
|
||||
// 更新对应物体的位置和朝向
|
||||
GameObject obj = GameObject.Find(objectId);
|
||||
if (obj != null)
|
||||
{
|
||||
Vector3 position = CoordinateConverter.ToUnityPosition(longitude, latitude);
|
||||
obj.transform.position = position;
|
||||
obj.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCollisionWarning(JObject json)
|
||||
{
|
||||
string object1Id = json["object1Id"].ToString();
|
||||
string object2Id = json["object2Id"].ToString();
|
||||
string warningLevel = json["warningLevel"].ToString();
|
||||
double distance = json["distance"].Value<double>();
|
||||
|
||||
// 显示碰撞警告效果
|
||||
WarningManager.Instance.ShowWarning(object1Id, object2Id, warningLevel, distance);
|
||||
}
|
||||
|
||||
private async void OnApplicationQuit()
|
||||
{
|
||||
await websocket.Close();
|
||||
}
|
||||
}
|
||||
@ -47,11 +47,13 @@ set(LIB_SOURCES
|
||||
src/core/System.cpp
|
||||
src/detector/CollisionDetector.cpp
|
||||
src/network/HTTPDataSource.cpp
|
||||
src/network/WebSocketServer.cpp
|
||||
src/spatial/AirportBounds.cpp
|
||||
src/spatial/CoordinateConverter.cpp
|
||||
src/types/BasicTypes.cpp
|
||||
src/types/VehicleData.cpp
|
||||
src/vehicle/ControllableVehicles.cpp
|
||||
src/config/SystemConfig.cpp
|
||||
)
|
||||
|
||||
# 创建主库
|
||||
|
||||
65
config/system_config.json
Normal file
65
config/system_config.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"airport": {
|
||||
"name": "青岛胶东国际机场",
|
||||
"iata": "TAO",
|
||||
"icao": "ZSQD",
|
||||
"reference_point": {
|
||||
"latitude": 36.361999,
|
||||
"longitude": 120.088003
|
||||
}
|
||||
},
|
||||
"data_source": {
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"aircraft_path": "/api/getCurrentFlightPositions",
|
||||
"vehicle_path": "/api/getCurrentVehiclePositions",
|
||||
"refresh_interval_ms": 1000,
|
||||
"timeout_ms": 5000
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"collision_detection": {
|
||||
"update_interval_ms": 200,
|
||||
"thresholds": {
|
||||
"runway": {
|
||||
"aircraft_ground": 100.0,
|
||||
"vehicle": 50.0
|
||||
},
|
||||
"taxiway": {
|
||||
"aircraft_ground": 50.0,
|
||||
"vehicle": 30.0
|
||||
},
|
||||
"apron": {
|
||||
"aircraft_ground": 40.0,
|
||||
"vehicle": 20.0
|
||||
},
|
||||
"service": {
|
||||
"aircraft_ground": 30.0,
|
||||
"vehicle": 15.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"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
|
||||
}
|
||||
}
|
||||
@ -120,7 +120,7 @@ class CollisionDetector {
|
||||
|
||||
#### 5.2.1 距离检测
|
||||
|
||||
1. 直接报警条件:
|
||||
1.直接报警条件:
|
||||
|
||||
```cpp
|
||||
if (distance < threshold * 0.5) { // 距离小于阈值的一半
|
||||
@ -128,7 +128,7 @@ if (distance < threshold * 0.5) { // 距离小于阈值的一半
|
||||
}
|
||||
```
|
||||
|
||||
2. 进一步检测条件:
|
||||
2.进一步检测条件:
|
||||
|
||||
```cpp
|
||||
if (distance < threshold) { // 距离在阈值范围内
|
||||
@ -138,7 +138,7 @@ if (distance < threshold) { // 距离在阈值范围内
|
||||
|
||||
#### 5.2.2 相对运动分析
|
||||
|
||||
1. 速度分量计算:
|
||||
1.速度分量计算:
|
||||
|
||||
```cpp
|
||||
// 考虑航向角,转换为数学坐标系
|
||||
@ -146,7 +146,7 @@ double vx = speed * std::cos((90 - heading) * M_PI / 180.0);
|
||||
double vy = speed * std::sin((90 - heading) * M_PI / 180.0);
|
||||
```
|
||||
|
||||
2. 相对运动计算:
|
||||
2.相对运动计算:
|
||||
|
||||
```cpp
|
||||
// 计算相对速度
|
||||
@ -157,7 +157,7 @@ double relativeVy = v1y - v2y;
|
||||
double relativeMotion = dx*relativeVx + dy*relativeVy;
|
||||
```
|
||||
|
||||
3. 碰撞判定:
|
||||
3.碰撞判定:
|
||||
|
||||
```cpp
|
||||
if (relativeMotion <= 0) { // 物体正在接近或相对静止
|
||||
@ -180,13 +180,13 @@ if (relativeMotion <= 0) { // 物体正在接近或相对静止
|
||||
|
||||
使用四叉树进行空间索引,优化查询性能:
|
||||
|
||||
1. 四叉树构建:
|
||||
1.四叉树构建:
|
||||
|
||||
```cpp
|
||||
QuadTree<Vehicle> vehicleTree_(bounds, 8); // 容量为8的四叉树
|
||||
```
|
||||
|
||||
2. 邻近查询:
|
||||
2.邻近查询:
|
||||
|
||||
```cpp
|
||||
auto nearbyVehicles = vehicleTree_.queryNearby(
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "CollisionDetector.h"
|
||||
#include "DataCollector.h"
|
||||
#include "airport/AirportBounds.h"
|
||||
#include "config/ConnectionConfig.h"
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct ControllableVehicleConfig {
|
||||
std::string vehicleNo;
|
||||
std::string ip;
|
||||
int port;
|
||||
};
|
||||
|
||||
class System {
|
||||
public:
|
||||
System();
|
||||
~System();
|
||||
|
||||
bool initialize(const ConnectionConfig& config);
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
private:
|
||||
std::unique_ptr<AirportBounds> airportBounds_;
|
||||
std::unique_ptr<DataCollector> dataCollector_;
|
||||
std::unique_ptr<CollisionDetector> collisionDetector_;
|
||||
std::vector<ControllableVehicleConfig> controllableVehicles_;
|
||||
|
||||
std::thread processThread_;
|
||||
bool running_ = false;
|
||||
|
||||
void processLoop();
|
||||
void processCollisions(const std::vector<CollisionRisk>& collisions);
|
||||
|
||||
bool loadAirportBounds();
|
||||
bool loadControllableVehicles();
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
struct ControllableVehicleConfig {
|
||||
std::string vehicleNo; // 车牌号
|
||||
std::string ip; // IP地址
|
||||
int port; // 端口号
|
||||
};
|
||||
|
||||
class ControllableVehicles {
|
||||
public:
|
||||
explicit ControllableVehicles(const std::string& configFile);
|
||||
virtual ~ControllableVehicles() = default;
|
||||
|
||||
// 获取所有可控车辆配置
|
||||
const std::vector<ControllableVehicleConfig>& getVehicles() const;
|
||||
|
||||
// 根据车牌号查找可控车辆配置
|
||||
const ControllableVehicleConfig* findVehicle(const std::string& vehicleNo) const;
|
||||
|
||||
// 检查车辆是否可控 - 添加 virtual
|
||||
virtual bool isControllable(const std::string& vehicleNo) const;
|
||||
|
||||
private:
|
||||
std::vector<ControllableVehicleConfig> vehicles_;
|
||||
void loadConfig(const std::string& configFile);
|
||||
};
|
||||
@ -2,15 +2,22 @@
|
||||
#include "network/HTTPDataSource.h"
|
||||
#include "utils/Logger.h"
|
||||
|
||||
DataCollector::DataCollector() = default;
|
||||
DataCollector::DataCollector()
|
||||
: lastSuccessfulFetch_(std::chrono::steady_clock::now()) // 初始化时间戳
|
||||
, last_warning_time_(std::chrono::steady_clock::now()) {
|
||||
}
|
||||
|
||||
DataCollector::~DataCollector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool DataCollector::initialize(const ConnectionConfig& config) {
|
||||
bool DataCollector::initialize(const DataSourceConfig& dataSourceConfig, const WarnConfig& warnConfig) {
|
||||
if (!dataSource_) {
|
||||
dataSource_ = std::make_shared<HTTPDataSource>(config.host, config.port);
|
||||
dataSourceConfig_ = dataSourceConfig;
|
||||
dataSource_ = std::make_shared<HTTPDataSource>(dataSourceConfig_);
|
||||
warnConfig_ = warnConfig;
|
||||
lastSuccessfulFetch_ = std::chrono::steady_clock::now(); // 重置时间戳
|
||||
last_warning_time_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -25,13 +32,17 @@ void DataCollector::start() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保连接成功
|
||||
running_ = true;
|
||||
lastSuccessfulFetch_ = std::chrono::steady_clock::now(); // 记录启动时间
|
||||
last_warning_time_ = lastSuccessfulFetch_; // 初始化告警时间
|
||||
|
||||
// 尝试连接,但即使失败也继续运行
|
||||
if (!dataSource_->connect()) {
|
||||
Logger::error("Failed to connect to data source");
|
||||
return;
|
||||
// 立即检查超时,这样如果一开始就连不上,会马上发出告警
|
||||
checkTimeout();
|
||||
}
|
||||
|
||||
running_ = true;
|
||||
collectorThread_ = std::thread(&DataCollector::collectLoop, this);
|
||||
}
|
||||
|
||||
@ -53,15 +64,66 @@ void DataCollector::stop() {
|
||||
void DataCollector::collectLoop() {
|
||||
while (running_) {
|
||||
loopCount_++;
|
||||
Logger::debug("Collection loop #", loopCount_.load());
|
||||
|
||||
if (fetchData()) {
|
||||
Logger::debug("Data fetched successfully in loop ", loopCount_.load());
|
||||
} else {
|
||||
bool success = fetchData();
|
||||
if (!success) {
|
||||
Logger::warning("Failed to fetch data in loop ", loopCount_.load());
|
||||
checkTimeout(); // 每次获取失败都检查超时
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(fetchInterval_);
|
||||
// 即使成功获取数据,也定期检查超时状态
|
||||
checkTimeout();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(dataSourceConfig_.refresh_interval_ms));
|
||||
}
|
||||
}
|
||||
|
||||
void DataCollector::checkTimeout() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - lastSuccessfulFetch_).count();
|
||||
|
||||
if (elapsed > dataSourceConfig_.timeout_ms) {
|
||||
// 检查是否达到告警间隔
|
||||
auto warning_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - last_warning_time_).count();
|
||||
|
||||
if (warning_elapsed >= warnConfig_.warning_interval_ms) {
|
||||
sendTimeoutWarning(elapsed);
|
||||
last_warning_time_ = now;
|
||||
Logger::warning("Data source timeout: No response for ", elapsed, "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DataCollector::resetTimeout() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - lastSuccessfulFetch_).count();
|
||||
|
||||
if (elapsed > dataSourceConfig_.timeout_ms) {
|
||||
// 如果之前处于超时状态,记录恢复日志
|
||||
Logger::info("Data source connection restored after ", elapsed, "ms timeout");
|
||||
}
|
||||
|
||||
lastSuccessfulFetch_ = now;
|
||||
}
|
||||
|
||||
void DataCollector::sendTimeoutWarning(int64_t elapsed_ms) {
|
||||
network::TimeoutWarningMessage msg;
|
||||
msg.type = "data_source_timeout"; // 确保类型正确
|
||||
msg.host = dataSourceConfig_.host;
|
||||
msg.port = dataSourceConfig_.port;
|
||||
msg.elapsed_ms = elapsed_ms;
|
||||
msg.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
|
||||
if (System::instance()) {
|
||||
System::instance()->broadcastTimeoutWarning(msg);
|
||||
Logger::debug("Sent timeout warning: host=", msg.host,
|
||||
", port=", msg.port,
|
||||
", elapsed=", msg.elapsed_ms, "ms");
|
||||
} else {
|
||||
Logger::error("Failed to send timeout warning: System instance not available");
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +133,8 @@ bool DataCollector::fetchData() {
|
||||
}
|
||||
|
||||
try {
|
||||
bool success = false;
|
||||
|
||||
// 获取航空器数据
|
||||
std::vector<Aircraft> aircraft;
|
||||
if (dataSource_->fetchAircraftData(aircraft)) {
|
||||
@ -99,6 +163,7 @@ bool DataCollector::fetchData() {
|
||||
}
|
||||
aircraftCache_ = std::move(aircraft);
|
||||
Logger::debug("Cached ", aircraftCache_.size(), " aircraft");
|
||||
success = true;
|
||||
}
|
||||
|
||||
// 获取车辆数据
|
||||
@ -123,9 +188,14 @@ bool DataCollector::fetchData() {
|
||||
}
|
||||
vehicleCache_ = std::move(vehicles);
|
||||
Logger::debug("Cached ", vehicleCache_.size(), " vehicles");
|
||||
success = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (success) {
|
||||
resetTimeout(); // 成功获取数据后重置超时
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
Logger::error("Exception in fetchData: ", e.what());
|
||||
@ -141,4 +211,52 @@ std::vector<Aircraft> DataCollector::getAircraftData() {
|
||||
std::vector<Vehicle> DataCollector::getVehicleData() {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||
return vehicleCache_;
|
||||
}
|
||||
|
||||
void DataCollector::refresh() {
|
||||
try {
|
||||
// 从数据源获取最新数据
|
||||
std::vector<Aircraft> newAircraft;
|
||||
std::vector<Vehicle> newVehicles;
|
||||
|
||||
if (dataSource_->fetchAircraftData(newAircraft)) {
|
||||
std::lock_guard<std::mutex> 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);
|
||||
} else {
|
||||
a.updateMotion(a.geo, a.timestamp);
|
||||
}
|
||||
lastAircraftPositions_[a.id] = a;
|
||||
}
|
||||
aircraftCache_ = std::move(newAircraft);
|
||||
}
|
||||
|
||||
if (dataSource_->fetchVehicleData(newVehicles)) {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||
// 更新运动信息
|
||||
for (auto& v : newVehicles) {
|
||||
auto it = lastVehiclePositions_.find(v.id);
|
||||
if (it != lastVehiclePositions_.end()) {
|
||||
// 复制历史记录
|
||||
v.copyHistoryFrom(it->second);
|
||||
// 更新运动信息
|
||||
v.updateMotion(v.geo, v.timestamp);
|
||||
} else {
|
||||
v.updateMotion(v.geo, v.timestamp);
|
||||
}
|
||||
lastVehiclePositions_[v.id] = v;
|
||||
}
|
||||
vehicleCache_ = std::move(newVehicles);
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
Logger::error("刷新数据失败: ", e.what());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@ -8,15 +8,16 @@
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
#include "DataSource.h"
|
||||
#include "DataSourceConfig.h"
|
||||
#include "types/BasicTypes.h"
|
||||
#include "network/ConnectionConfig.h"
|
||||
#include "config/WarnConfig.h"
|
||||
|
||||
class DataCollector {
|
||||
public:
|
||||
DataCollector();
|
||||
~DataCollector();
|
||||
|
||||
bool initialize(const ConnectionConfig& config);
|
||||
bool initialize(const DataSourceConfig& config, const WarnConfig& warnConfig);
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
@ -28,9 +29,14 @@ public:
|
||||
void setDataSource(std::shared_ptr<DataSource> source) {
|
||||
dataSource_ = source;
|
||||
}
|
||||
|
||||
// 添加刷新方法
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
std::shared_ptr<DataSource> dataSource_;
|
||||
DataSourceConfig dataSourceConfig_;
|
||||
WarnConfig warnConfig_;
|
||||
std::thread collectorThread_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<uint64_t> loopCount_{0};
|
||||
@ -44,9 +50,15 @@ private:
|
||||
std::map<std::string, Aircraft> lastAircraftPositions_;
|
||||
std::map<std::string, Vehicle> lastVehiclePositions_;
|
||||
|
||||
std::chrono::steady_clock::time_point lastFetchTime_;
|
||||
std::chrono::milliseconds fetchInterval_{1000};
|
||||
|
||||
// 超时检测相关
|
||||
std::chrono::steady_clock::time_point lastSuccessfulFetch_;
|
||||
std::chrono::steady_clock::time_point last_warning_time_;
|
||||
std::chrono::steady_clock::time_point last_log_time_;
|
||||
|
||||
void checkTimeout();
|
||||
void resetTimeout();
|
||||
void sendTimeoutWarning(int64_t elapsed_ms);
|
||||
|
||||
void collectLoop();
|
||||
bool fetchData();
|
||||
};
|
||||
|
||||
15
src/collector/DataSourceConfig.h
Normal file
15
src/collector/DataSourceConfig.h
Normal file
@ -0,0 +1,15 @@
|
||||
#ifndef AIRPORT_COLLECTOR_DATA_SOURCE_CONFIG_H
|
||||
#define AIRPORT_COLLECTOR_DATA_SOURCE_CONFIG_H
|
||||
|
||||
#include <string>
|
||||
|
||||
struct DataSourceConfig {
|
||||
std::string host;
|
||||
uint16_t port;
|
||||
std::string aircraft_path;
|
||||
std::string vehicle_path;
|
||||
int refresh_interval_ms;
|
||||
int timeout_ms;
|
||||
};
|
||||
|
||||
#endif
|
||||
75
src/config/SystemConfig.cpp
Normal file
75
src/config/SystemConfig.cpp
Normal file
@ -0,0 +1,75 @@
|
||||
#include "config/SystemConfig.h"
|
||||
#include "utils/Logger.h"
|
||||
#include <fstream>
|
||||
|
||||
SystemConfig SystemConfig::load(const std::string& filename) {
|
||||
try {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Cannot open config file: " + filename);
|
||||
}
|
||||
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
|
||||
SystemConfig config;
|
||||
|
||||
// 加载机场信息
|
||||
config.airport.name = j["airport"]["name"];
|
||||
config.airport.iata = j["airport"]["iata"];
|
||||
config.airport.icao = j["airport"]["icao"];
|
||||
config.airport.reference_point.latitude = j["airport"]["reference_point"]["latitude"];
|
||||
config.airport.reference_point.longitude = j["airport"]["reference_point"]["longitude"];
|
||||
|
||||
// 加载数据源配置
|
||||
config.data_source.host = j["data_source"]["host"];
|
||||
config.data_source.port = j["data_source"]["port"];
|
||||
config.data_source.aircraft_path = j["data_source"]["aircraft_path"];
|
||||
config.data_source.vehicle_path = j["data_source"]["vehicle_path"];
|
||||
config.data_source.refresh_interval_ms = j["data_source"]["refresh_interval_ms"];
|
||||
config.data_source.timeout_ms = j["data_source"]["timeout_ms"];
|
||||
|
||||
// 加载 WebSocket 配置
|
||||
config.websocket.port = j["websocket"]["port"];
|
||||
config.websocket.max_connections = j["websocket"]["max_connections"];
|
||||
config.websocket.ping_interval_ms = j["websocket"]["ping_interval_ms"];
|
||||
config.websocket.position_update.aircraft_interval_ms = j["websocket"]["position_update"]["aircraft_interval_ms"];
|
||||
config.websocket.position_update.vehicle_interval_ms = j["websocket"]["position_update"]["vehicle_interval_ms"];
|
||||
|
||||
// 加载碰撞检测配置
|
||||
config.collision_detection.update_interval_ms = j["collision_detection"]["update_interval_ms"];
|
||||
|
||||
// 加载阈值配置
|
||||
auto& thresholds = j["collision_detection"]["thresholds"];
|
||||
config.collision_detection.thresholds.runway.aircraft_ground = thresholds["runway"]["aircraft_ground"];
|
||||
config.collision_detection.thresholds.runway.vehicle = thresholds["runway"]["vehicle"];
|
||||
config.collision_detection.thresholds.taxiway.aircraft_ground = thresholds["taxiway"]["aircraft_ground"];
|
||||
config.collision_detection.thresholds.taxiway.vehicle = thresholds["taxiway"]["vehicle"];
|
||||
config.collision_detection.thresholds.apron.aircraft_ground = thresholds["apron"]["aircraft_ground"];
|
||||
config.collision_detection.thresholds.apron.vehicle = thresholds["apron"]["vehicle"];
|
||||
config.collision_detection.thresholds.service.aircraft_ground = thresholds["service"]["aircraft_ground"];
|
||||
config.collision_detection.thresholds.service.vehicle = thresholds["service"]["vehicle"];
|
||||
|
||||
// 加载日志配置
|
||||
config.logging.level = j["logging"]["level"];
|
||||
config.logging.file = j["logging"]["file"];
|
||||
config.logging.max_size_mb = j["logging"]["max_size_mb"];
|
||||
config.logging.max_files = j["logging"]["max_files"];
|
||||
config.logging.console_output = j["logging"]["console_output"];
|
||||
|
||||
// 加载调试配置
|
||||
config.debug.enable_mock_data = j["debug"]["enable_mock_data"];
|
||||
config.debug.save_raw_data = j["debug"]["save_raw_data"];
|
||||
config.debug.profile_performance = j["debug"]["profile_performance"];
|
||||
|
||||
// 加载告警配置
|
||||
config.warning.warning_interval_ms = j["warning"]["warning_interval_ms"];
|
||||
config.warning.log_interval_ms = j["warning"]["log_interval_ms"];
|
||||
|
||||
return config;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
Logger::error("Failed to load system config: ", e.what());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
70
src/config/SystemConfig.h
Normal file
70
src/config/SystemConfig.h
Normal file
@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
struct SystemConfig {
|
||||
struct Airport {
|
||||
std::string name;
|
||||
std::string iata;
|
||||
std::string icao;
|
||||
struct {
|
||||
double latitude;
|
||||
double longitude;
|
||||
} reference_point;
|
||||
} airport;
|
||||
|
||||
struct DataSource {
|
||||
std::string host;
|
||||
uint16_t port;
|
||||
std::string aircraft_path;
|
||||
std::string vehicle_path;
|
||||
int refresh_interval_ms;
|
||||
int timeout_ms;
|
||||
} data_source;
|
||||
|
||||
struct WebSocket {
|
||||
int port;
|
||||
int max_connections;
|
||||
int ping_interval_ms;
|
||||
struct PositionUpdate {
|
||||
int aircraft_interval_ms;
|
||||
int vehicle_interval_ms;
|
||||
} position_update;
|
||||
} websocket;
|
||||
|
||||
struct CollisionDetection {
|
||||
int update_interval_ms;
|
||||
struct Thresholds {
|
||||
struct Area {
|
||||
double aircraft_ground;
|
||||
double vehicle;
|
||||
};
|
||||
Area runway;
|
||||
Area taxiway;
|
||||
Area apron;
|
||||
Area service;
|
||||
} thresholds;
|
||||
} 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;
|
||||
|
||||
static SystemConfig load(const std::string& filename);
|
||||
};
|
||||
11
src/config/WarnConfig.h
Normal file
11
src/config/WarnConfig.h
Normal file
@ -0,0 +1,11 @@
|
||||
#ifndef AIRPORT_NETWORK_WARN_CONFIG_H
|
||||
#define AIRPORT_NETWORK_WARN_CONFIG_H
|
||||
|
||||
#include <string>
|
||||
|
||||
struct WarnConfig {
|
||||
int warning_interval_ms;
|
||||
int log_interval_ms;
|
||||
};
|
||||
|
||||
#endif // AIRPORT_NETWORK_WARN_CONFIG_H
|
||||
@ -1,29 +1,72 @@
|
||||
#include <csignal>
|
||||
#include <typeinfo>
|
||||
#include <chrono>
|
||||
#include "core/System.h"
|
||||
#include "utils/Logger.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "collector/DataCollector.h"
|
||||
|
||||
System::System() = default;
|
||||
System* System::instance_ = nullptr;
|
||||
|
||||
System::System() {
|
||||
instance_ = this;
|
||||
std::signal(SIGINT, signalHandler);
|
||||
std::signal(SIGTERM, signalHandler);
|
||||
}
|
||||
|
||||
System::~System() {
|
||||
stop();
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
bool System::initialize(const ConnectionConfig& config) {
|
||||
void System::signalHandler(int signal) {
|
||||
Logger::info("Received signal: ", signal);
|
||||
if (instance_) {
|
||||
instance_->stop();
|
||||
}
|
||||
std::exit(0);
|
||||
}
|
||||
|
||||
bool System::initialize() {
|
||||
try {
|
||||
// 加载系统配置
|
||||
system_config_ = SystemConfig::load("config/system_config.json");
|
||||
|
||||
// 初始化 WebSocket 服务器
|
||||
ws_server_ = std::make_unique<network::WebSocketServer>(system_config_.websocket.port);
|
||||
ws_thread_ = std::thread([this]() {
|
||||
ws_server_->start();
|
||||
});
|
||||
|
||||
// 加载机场区域配置
|
||||
airportBounds_ = std::make_unique<AirportBounds>("config/airport_bounds.json");
|
||||
|
||||
// 加载可控车辆配置
|
||||
controllableVehicles_ = std::make_unique<ControllableVehicles>("config/controllable_vehicles.json");
|
||||
|
||||
// 初始化数据采集器
|
||||
dataCollector_ = std::make_unique<DataCollector>();
|
||||
|
||||
|
||||
// 初始化碰撞检测器
|
||||
collisionDetector_ = std::make_unique<CollisionDetector>(*airportBounds_, *controllableVehicles_);
|
||||
|
||||
// 初始化数据采集器
|
||||
return dataCollector_->initialize(config);
|
||||
|
||||
// 创建数据采集器
|
||||
dataCollector_ = std::make_unique<DataCollector>();
|
||||
|
||||
// 数据采集器初始化并启动
|
||||
DataSourceConfig dataSourceConfig{
|
||||
system_config_.data_source.host,
|
||||
system_config_.data_source.port,
|
||||
system_config_.data_source.aircraft_path,
|
||||
system_config_.data_source.vehicle_path,
|
||||
system_config_.data_source.refresh_interval_ms,
|
||||
system_config_.data_source.timeout_ms
|
||||
};
|
||||
|
||||
WarnConfig warnConfig{
|
||||
system_config_.warning.warning_interval_ms,
|
||||
system_config_.warning.log_interval_ms
|
||||
};
|
||||
|
||||
return dataCollector_->initialize(dataSourceConfig, warnConfig);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
Logger::error("Failed to initialize system: ", e.what());
|
||||
@ -48,6 +91,17 @@ void System::stop() {
|
||||
}
|
||||
|
||||
running_ = false;
|
||||
|
||||
// 停止 WebSocket 服务器
|
||||
if (ws_server_) {
|
||||
ws_server_->stop();
|
||||
}
|
||||
|
||||
// 等待线程结束
|
||||
if (ws_thread_.joinable()) {
|
||||
ws_thread_.join();
|
||||
}
|
||||
|
||||
if (processThread_.joinable()) {
|
||||
processThread_.join();
|
||||
}
|
||||
@ -55,27 +109,64 @@ void System::stop() {
|
||||
if (dataCollector_) {
|
||||
dataCollector_->stop();
|
||||
}
|
||||
|
||||
Logger::info("System stopped");
|
||||
}
|
||||
|
||||
void System::processLoop() {
|
||||
auto last_aircraft_update = std::chrono::steady_clock::now();
|
||||
auto last_vehicle_update = std::chrono::steady_clock::now();
|
||||
auto last_data_refresh = std::chrono::steady_clock::now();
|
||||
|
||||
while (running_) {
|
||||
try {
|
||||
// 获取最新数据
|
||||
auto aircraft = dataCollector_->getAircraftData();
|
||||
auto vehicles = dataCollector_->getVehicleData();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// 更新碰撞检测器
|
||||
collisionDetector_->updateTraffic(aircraft, vehicles);
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - last_data_refresh).count() >= system_config_.data_source.refresh_interval_ms) {
|
||||
dataCollector_->refresh();
|
||||
last_data_refresh = now;
|
||||
|
||||
auto aircraft = dataCollector_->getAircraftData();
|
||||
auto vehicles = dataCollector_->getVehicleData();
|
||||
|
||||
Logger::debug("Got data: ", aircraft.size(), " aircraft, ", vehicles.size(), " vehicles");
|
||||
|
||||
// 检查航空器更新
|
||||
auto aircraft_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - last_aircraft_update).count();
|
||||
if (aircraft_elapsed >= system_config_.websocket.position_update.aircraft_interval_ms) {
|
||||
Logger::debug("Broadcasting aircraft positions (", aircraft.size(), " aircraft)");
|
||||
for (const auto& ac : aircraft) {
|
||||
broadcastPositionUpdate(ac);
|
||||
}
|
||||
last_aircraft_update = now;
|
||||
}
|
||||
|
||||
// 检查车辆更新
|
||||
auto vehicle_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - last_vehicle_update).count();
|
||||
if (vehicle_elapsed >= system_config_.websocket.position_update.vehicle_interval_ms) {
|
||||
Logger::debug("Broadcasting vehicle positions (", vehicles.size(), " vehicles)");
|
||||
for (const auto& veh : vehicles) {
|
||||
broadcastPositionUpdate(veh);
|
||||
}
|
||||
last_vehicle_update = now;
|
||||
}
|
||||
|
||||
// 更新碰撞检测器
|
||||
collisionDetector_->updateTraffic(aircraft, vehicles);
|
||||
auto collisions = collisionDetector_->detectCollisions();
|
||||
|
||||
if (!collisions.empty()) {
|
||||
processCollisions(collisions);
|
||||
}
|
||||
}
|
||||
|
||||
// 检测碰撞
|
||||
auto collisions = collisionDetector_->detectCollisions();
|
||||
|
||||
// 处理碰撞警告
|
||||
processCollisions(collisions);
|
||||
|
||||
// 处理间隔
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
// 处理间隔设置为数据刷新间隔的 1/10
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(
|
||||
system_config_.data_source.refresh_interval_ms / 10
|
||||
));
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
Logger::error("处理循环发生错误: ", e.what());
|
||||
@ -88,6 +179,9 @@ void System::processCollisions(const std::vector<CollisionRisk>& collisions) {
|
||||
Logger::info("检测到 ", collisions.size(), " 个碰撞风险");
|
||||
|
||||
for (const auto& risk : collisions) {
|
||||
// 发送碰撞警告到 Unity 客户端
|
||||
broadcastCollisionWarning(risk);
|
||||
|
||||
// 根据风险等级选择不同的日志级别
|
||||
switch (risk.level) {
|
||||
case RiskLevel::EMERGENCY:
|
||||
@ -112,4 +206,69 @@ void System::processCollisions(const std::vector<CollisionRisk>& collisions) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void System::broadcastPositionUpdate(const MovingObject& obj) {
|
||||
network::PositionUpdateMessage msg;
|
||||
msg.objectId = obj.id;
|
||||
msg.objectType = (typeid(obj) == typeid(Aircraft)) ? "aircraft" : "vehicle";
|
||||
msg.longitude = obj.geo.longitude;
|
||||
msg.latitude = obj.geo.latitude;
|
||||
msg.heading = obj.heading;
|
||||
msg.speed = obj.speed;
|
||||
msg.timestamp = obj.timestamp;
|
||||
|
||||
if (ws_server_) {
|
||||
ws_server_->broadcast(msg.toJson().dump());
|
||||
Logger::debug("Broadcast position update: type=", msg.objectType,
|
||||
", id=", msg.objectId,
|
||||
", pos=(", msg.longitude, ",", msg.latitude, ")");
|
||||
}
|
||||
}
|
||||
|
||||
void System::broadcastCollisionWarning(const CollisionRisk& risk) {
|
||||
network::CollisionWarningMessage msg;
|
||||
msg.id1 = risk.id1;
|
||||
msg.id2 = risk.id2;
|
||||
|
||||
// 根据风险等级设置预警级别和阈值
|
||||
switch (risk.level) {
|
||||
case RiskLevel::EMERGENCY:
|
||||
msg.warningLevel = "high";
|
||||
msg.threshold = system_config_.collision_detection.thresholds.runway.aircraft_ground;
|
||||
break;
|
||||
case RiskLevel::CRITICAL:
|
||||
msg.warningLevel = "medium";
|
||||
msg.threshold = system_config_.collision_detection.thresholds.taxiway.aircraft_ground;
|
||||
break;
|
||||
case RiskLevel::WARNING:
|
||||
msg.warningLevel = "low";
|
||||
msg.threshold = system_config_.collision_detection.thresholds.apron.aircraft_ground;
|
||||
break;
|
||||
default:
|
||||
return; // 不发送无风险的警告
|
||||
}
|
||||
|
||||
msg.distance = risk.distance;
|
||||
msg.relativeSpeed = risk.relativeSpeed;
|
||||
msg.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
|
||||
if (ws_server_) {
|
||||
ws_server_->broadcast(msg.toJson().dump());
|
||||
}
|
||||
}
|
||||
|
||||
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.host, ":", warning.port, " - ",
|
||||
warning.elapsed_ms, "ms without response");
|
||||
} else {
|
||||
Logger::warning("Connection timeout: ", warning.host, ":", warning.port, " - ",
|
||||
warning.elapsed_ms, "ms without response");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,56 @@
|
||||
#ifndef AIRPORT_CORE_SYSTEM_H
|
||||
#define AIRPORT_CORE_SYSTEM_H
|
||||
#pragma once
|
||||
|
||||
#include "collector/DataCollector.h"
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include "types/BasicTypes.h"
|
||||
#include "detector/CollisionDetector.h"
|
||||
#include "spatial/AirportBounds.h"
|
||||
#include "network/ConnectionConfig.h"
|
||||
#include "vehicle/ControllableVehicles.h"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include "network/WebSocketServer.h"
|
||||
#include "network/MessageTypes.h"
|
||||
#include "config/SystemConfig.h"
|
||||
|
||||
// 前向声明
|
||||
class DataCollector;
|
||||
|
||||
class System {
|
||||
public:
|
||||
System();
|
||||
~System();
|
||||
|
||||
bool initialize(const ConnectionConfig& config);
|
||||
bool initialize();
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
static System* instance() { return instance_; }
|
||||
static void signalHandler(int signal);
|
||||
|
||||
void broadcastTimeoutWarning(const network::TimeoutWarningMessage& warning);
|
||||
|
||||
private:
|
||||
std::unique_ptr<ControllableVehicles> controllableVehicles_;
|
||||
std::unique_ptr<DataCollector> dataCollector_;
|
||||
std::unique_ptr<CollisionDetector> collisionDetector_;
|
||||
std::unique_ptr<AirportBounds> airportBounds_;
|
||||
|
||||
std::thread processThread_;
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
void processLoop();
|
||||
void processCollisions(const std::vector<CollisionRisk>& collisions);
|
||||
|
||||
// WebSocket 相关方法
|
||||
void broadcastPositionUpdate(const MovingObject& obj);
|
||||
void broadcastCollisionWarning(const CollisionRisk& risk);
|
||||
|
||||
bool loadAirportBounds();
|
||||
bool loadControllableVehicles();
|
||||
};
|
||||
|
||||
#endif // AIRPORT_CORE_SYSTEM_H
|
||||
std::atomic<bool> running_{false};
|
||||
std::thread processThread_;
|
||||
|
||||
std::unique_ptr<AirportBounds> airportBounds_;
|
||||
std::unique_ptr<ControllableVehicles> controllableVehicles_;
|
||||
std::unique_ptr<CollisionDetector> collisionDetector_;
|
||||
std::unique_ptr<DataCollector> dataCollector_;
|
||||
|
||||
// WebSocket 服务器
|
||||
std::unique_ptr<network::WebSocketServer> ws_server_;
|
||||
std::thread ws_thread_;
|
||||
|
||||
// 系统配置
|
||||
SystemConfig system_config_;
|
||||
|
||||
static System* instance_;
|
||||
};
|
||||
@ -116,7 +116,6 @@ std::vector<CollisionRisk> CollisionDetector::detectCollisions() {
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info("Collision detection completed, found ", risks.size(), " risks");
|
||||
return risks;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include "core/System.h"
|
||||
#include "utils/Logger.h"
|
||||
#include "network/ConnectionConfig.h"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
@ -23,13 +22,7 @@ int main() {
|
||||
|
||||
// 初始化系统
|
||||
System system;
|
||||
ConnectionConfig config{
|
||||
.host = "localhost",
|
||||
.port = 8080,
|
||||
.timeout = 1000 // 1秒超时
|
||||
};
|
||||
|
||||
if (!system.initialize(config)) {
|
||||
if (!system.initialize()) {
|
||||
Logger::error("Failed to initialize system");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
#ifndef AIRPORT_NETWORK_CONNECTION_CONFIG_H
|
||||
#define AIRPORT_NETWORK_CONNECTION_CONFIG_H
|
||||
|
||||
#include <string>
|
||||
|
||||
struct ConnectionConfig {
|
||||
std::string host; // 服务器主机名
|
||||
int port; // 服务器端口
|
||||
int timeout; // 连接超时时间(毫秒)
|
||||
};
|
||||
|
||||
#endif // AIRPORT_NETWORK_CONNECTION_CONFIG_H
|
||||
@ -5,9 +5,10 @@
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
HTTPDataSource::HTTPDataSource(const std::string& host, uint16_t port)
|
||||
: host_(host)
|
||||
, port_(std::to_string(port))
|
||||
HTTPDataSource::HTTPDataSource(const DataSourceConfig& config)
|
||||
: config_(config)
|
||||
, host_(config.host)
|
||||
, port_(std::to_string(config.port))
|
||||
, socket_(std::make_unique<asio::ip::tcp::socket>(io_context_)) {
|
||||
}
|
||||
|
||||
@ -46,44 +47,88 @@ bool HTTPDataSource::isAvailable() const {
|
||||
return socket_ && socket_->is_open();
|
||||
}
|
||||
|
||||
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<std::chrono::milliseconds>(
|
||||
now - last_connect_attempt_).count();
|
||||
|
||||
if (elapsed < config_.refresh_interval_ms) {
|
||||
is_reconnecting_ = false;
|
||||
return false; // 距离上次尝试时间太短
|
||||
}
|
||||
|
||||
Logger::info("Attempting to reconnect to ", config_.host, ":", config_.port);
|
||||
last_connect_attempt_ = now;
|
||||
|
||||
for (int retry = 0; retry < MAX_RETRIES; ++retry) {
|
||||
if (connect()) {
|
||||
Logger::info("Successfully reconnected");
|
||||
is_reconnecting_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (retry < MAX_RETRIES - 1) {
|
||||
Logger::info("Reconnection attempt ", retry + 1, " failed, retrying...");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(config_.refresh_interval_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::ensureConnected() {
|
||||
if (isAvailable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return tryReconnect();
|
||||
}
|
||||
|
||||
bool HTTPDataSource::fetchAircraftData(std::vector<Aircraft>& aircraft) {
|
||||
// 首先检查连接状态
|
||||
if (!isAvailable()) {
|
||||
Logger::error("Cannot fetch aircraft data: not connected");
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!ensureConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
std::string response;
|
||||
if (!sendRequest("/api/getCurrentFlightPositions") || !readResponse(response)) {
|
||||
disconnect(); // 如果请求失败,断开连接
|
||||
if (!sendRequest(config_.aircraft_path) || !readResponse(response)) {
|
||||
disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parseAircraftResponse(response, aircraft)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return parseAircraftResponse(response, aircraft);
|
||||
}
|
||||
|
||||
bool HTTPDataSource::fetchVehicleData(std::vector<Vehicle>& vehicles) {
|
||||
// 首先检查连接状态
|
||||
if (!isAvailable()) {
|
||||
Logger::error("Cannot fetch vehicle data: not connected");
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
if (!ensureConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
std::string response;
|
||||
if (!sendRequest("/api/getCurrentVehiclePositions") || !readResponse(response)) {
|
||||
disconnect(); // 如果请求失败,断开连接
|
||||
if (!sendRequest(config_.vehicle_path) || !readResponse(response)) {
|
||||
disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parseVehicleResponse(response, vehicles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return parseVehicleResponse(response, vehicles);
|
||||
}
|
||||
|
||||
bool HTTPDataSource::sendRequest(const std::string& path) {
|
||||
|
||||
@ -5,14 +5,20 @@
|
||||
#include "spatial/CoordinateConverter.h"
|
||||
#include <boost/asio.hpp>
|
||||
#include <string>
|
||||
#include "collector/DataSourceConfig.h"
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include "core/System.h"
|
||||
|
||||
namespace asio = boost::asio;
|
||||
|
||||
class HTTPDataSource : public DataSource {
|
||||
public:
|
||||
explicit HTTPDataSource(const std::string& host, uint16_t port);
|
||||
explicit HTTPDataSource(const DataSourceConfig& config);
|
||||
~HTTPDataSource() override;
|
||||
|
||||
DataSourceConfig config_;
|
||||
bool connect() override;
|
||||
void disconnect() override;
|
||||
bool isAvailable() const override;
|
||||
@ -26,6 +32,14 @@ private:
|
||||
asio::io_context io_context_;
|
||||
std::unique_ptr<asio::ip::tcp::socket> socket_;
|
||||
CoordinateConverter coordinateConverter_;
|
||||
std::mutex mutex_;
|
||||
|
||||
std::chrono::steady_clock::time_point last_connect_attempt_;
|
||||
static constexpr int MAX_RETRIES = 3; // 单次连接最大重试次数
|
||||
std::atomic<bool> is_reconnecting_{false}; // 重连状态标志
|
||||
|
||||
bool tryReconnect();
|
||||
bool ensureConnected();
|
||||
|
||||
bool sendRequest(const std::string& path);
|
||||
bool readResponse(std::string& response);
|
||||
@ -33,4 +47,4 @@ private:
|
||||
bool parseVehicleResponse(const std::string& response, std::vector<Vehicle>& vehicles);
|
||||
};
|
||||
|
||||
#endif // AIRPORT_NETWORK_HTTP_DATA_SOURCE_H
|
||||
#endif // AIRPORT_NETWORK_HTTP_DATA_SOURCE_H
|
||||
|
||||
75
src/network/MessageTypes.h
Normal file
75
src/network/MessageTypes.h
Normal file
@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
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 type = "data_source_timeout";
|
||||
std::string host;
|
||||
int port;
|
||||
int64_t elapsed_ms;
|
||||
uint64_t timestamp;
|
||||
|
||||
nlohmann::json toJson() const {
|
||||
return {
|
||||
{"type", type},
|
||||
{"host", host},
|
||||
{"port", port},
|
||||
{"elapsed_ms", elapsed_ms},
|
||||
{"timestamp", timestamp}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
118
src/network/WebSocketServer.cpp
Normal file
118
src/network/WebSocketServer.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include "network/WebSocketServer.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace network {
|
||||
|
||||
WebSocketServer::WebSocketServer(uint16_t port)
|
||||
: acceptor_(ioc_, {boost::asio::ip::tcp::v4(), port}) {
|
||||
}
|
||||
|
||||
WebSocketServer::~WebSocketServer() {
|
||||
// 关闭所有连接
|
||||
std::lock_guard<std::mutex> 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() {
|
||||
handleAccept();
|
||||
ioc_.run();
|
||||
}
|
||||
|
||||
void WebSocketServer::broadcast(const std::string& message) {
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
auto it = sessions_.begin();
|
||||
while (it != sessions_.end()) {
|
||||
if (auto session = it->lock()) { // 获取 shared_ptr
|
||||
try {
|
||||
session->write(boost::asio::buffer(message));
|
||||
++it;
|
||||
} catch (...) {
|
||||
it = sessions_.erase(it); // 移除失败的会话
|
||||
}
|
||||
} else {
|
||||
it = sessions_.erase(it); // 移除已失效的会话
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketServer::handleAccept() {
|
||||
acceptor_.async_accept(
|
||||
[this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) {
|
||||
if (!ec) {
|
||||
// 创建新的 WebSocket 会话
|
||||
auto ws = std::make_shared<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>(std::move(socket));
|
||||
|
||||
// 异步完成 WebSocket 握手
|
||||
ws->async_accept(
|
||||
[this, ws](boost::system::error_code ec) {
|
||||
if (!ec) {
|
||||
// 握手成功,保存会话并开始读取消息
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
sessions_.push_back(ws); // 存储 weak_ptr
|
||||
}
|
||||
doRead(ws);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 继续接受新的连接
|
||||
handleAccept();
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketServer::doRead(std::shared_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>> ws) {
|
||||
// 为每个会话创建一个专用的缓冲区
|
||||
auto buffer = std::make_shared<boost::beast::flat_buffer>();
|
||||
|
||||
// 异步读取消息
|
||||
ws->async_read(
|
||||
*buffer,
|
||||
[this, ws, buffer](boost::system::error_code ec, std::size_t bytes_transferred) {
|
||||
if (!ec) {
|
||||
// 成功读取消息,继续读取下一条
|
||||
buffer->consume(buffer->size()); // 清空缓冲区
|
||||
doRead(ws);
|
||||
} else {
|
||||
// 发生错误或连接关闭,移除会话
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
auto it = std::find_if(sessions_.begin(), sessions_.end(),
|
||||
[ws](const std::weak_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>& weak) {
|
||||
return !weak.expired() && weak.lock() == ws;
|
||||
});
|
||||
if (it != sessions_.end()) {
|
||||
sessions_.erase(it);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WebSocketServer::stop() {
|
||||
running_ = false;
|
||||
ioc_.stop(); // 停止 io_context
|
||||
|
||||
// 关闭所有连接
|
||||
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
||||
for (auto& session : sessions_) {
|
||||
if (auto ws = session.lock()) {
|
||||
try {
|
||||
ws->close(boost::beast::websocket::close_code::normal);
|
||||
} catch (...) {
|
||||
// 忽略关闭时的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
sessions_.clear();
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
32
src/network/WebSocketServer.h
Normal file
32
src/network/WebSocketServer.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <boost/beast/websocket.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
|
||||
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<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>> ws);
|
||||
|
||||
boost::asio::io_context ioc_;
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
std::vector<std::weak_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>> sessions_;
|
||||
std::mutex sessions_mutex_;
|
||||
std::atomic<bool> running_{true};
|
||||
};
|
||||
|
||||
} // namespace network
|
||||
@ -131,4 +131,17 @@ void MovingObject::updateMotion(const GeoPosition& newPos, uint64_t newTime) {
|
||||
|
||||
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米才认为发生了变化
|
||||
}
|
||||
@ -1,28 +1,23 @@
|
||||
#ifndef AIRPORT_TYPES_BASIC_TYPES_H
|
||||
#define AIRPORT_TYPES_BASIC_TYPES_H
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
|
||||
// 基础数据类型
|
||||
struct Vector2D {
|
||||
double x; // 东西方向(米)
|
||||
double y; // 南北方向(米)
|
||||
|
||||
Vector2D() : x(0), y(0) {}
|
||||
Vector2D(double x_, double y_) : x(x_), y(y_) {}
|
||||
double x;
|
||||
double y;
|
||||
|
||||
double magnitude() const;
|
||||
double direction() const;
|
||||
};
|
||||
|
||||
struct GeoPosition {
|
||||
double latitude; // 纬度
|
||||
double longitude; // 经度
|
||||
double latitude;
|
||||
double longitude;
|
||||
};
|
||||
|
||||
// 历史位置记录
|
||||
struct PositionRecord {
|
||||
GeoPosition geo;
|
||||
uint64_t timestamp;
|
||||
@ -30,71 +25,57 @@ struct PositionRecord {
|
||||
PositionRecord(const GeoPosition& g, uint64_t t) : geo(g), timestamp(t) {}
|
||||
};
|
||||
|
||||
// 基础移动物体数据
|
||||
// 移动物体基类
|
||||
class MovingObject {
|
||||
public:
|
||||
std::string id; // 唯一标识
|
||||
GeoPosition geo; // 地理坐标
|
||||
Vector2D position; // 平面坐标
|
||||
double heading; // 航向角(度)
|
||||
double speed; // 速度(米/秒)
|
||||
uint64_t timestamp; // 时间戳
|
||||
|
||||
// 更新运动信息
|
||||
virtual void updateMotion(const GeoPosition& newPos, uint64_t newTime);
|
||||
virtual ~MovingObject() = default; // 添加虚析构函数以支持 dynamic_cast
|
||||
|
||||
// 复制历史记录
|
||||
void copyHistoryFrom(const MovingObject& other);
|
||||
std::string id;
|
||||
GeoPosition geo;
|
||||
Vector2D position;
|
||||
double heading;
|
||||
double speed;
|
||||
uint64_t timestamp;
|
||||
|
||||
MovingObject() : heading(0), speed(0), timestamp(0) {}
|
||||
virtual ~MovingObject() = default;
|
||||
|
||||
protected:
|
||||
static constexpr size_t MAX_HISTORY = 5; // 保留最近5个位置记录
|
||||
std::deque<PositionRecord> positionHistory; // 位置历史记录
|
||||
std::deque<PositionRecord> 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 = 0;
|
||||
virtual double getSpeedSmoothingFactor() const { return 0.3; }
|
||||
|
||||
void updateMotion(const GeoPosition& newPos, uint64_t newTime);
|
||||
void copyHistoryFrom(const MovingObject& other);
|
||||
bool hasPositionChanged() const;
|
||||
|
||||
static constexpr size_t MAX_HISTORY = 10;
|
||||
};
|
||||
|
||||
// 航空器数据
|
||||
struct Aircraft : MovingObject {
|
||||
std::string flightNo; // 航班号
|
||||
std::string trackNumber; // 航迹号
|
||||
double altitude; // 高度(米)
|
||||
|
||||
protected:
|
||||
// 航空器特定参数
|
||||
static constexpr double MAX_SPEED = 100.0; // 最大速度(米/秒)
|
||||
static constexpr double MAX_POSITION_JUMP = 150.0; // 最大位置跳变(米)
|
||||
static constexpr double SPEED_SMOOTHING_FACTOR = 0.3; // 速度平滑因子
|
||||
// 航空器类
|
||||
class Aircraft : public MovingObject {
|
||||
public:
|
||||
std::string flightNo;
|
||||
std::string trackNumber;
|
||||
double altitude;
|
||||
|
||||
bool isValidPosition(const GeoPosition& newPos) const override;
|
||||
bool isValidSpeed(double speed) const override;
|
||||
double getSpeedSmoothingFactor() const override { return SPEED_SMOOTHING_FACTOR; }
|
||||
};
|
||||
|
||||
// 车辆数据
|
||||
struct Vehicle : MovingObject {
|
||||
std::string vehicleNo; // 车牌号
|
||||
|
||||
protected:
|
||||
// 车辆特定参数
|
||||
static constexpr double MAX_SPEED = 30.0; // 最大速度(米/秒)
|
||||
|
||||
static constexpr double MAX_SPEED = 100.0; // 最大速度(米/秒)
|
||||
static constexpr double MAX_POSITION_JUMP = 50.0; // 最大位置跳变(米)
|
||||
static constexpr double SPEED_SMOOTHING_FACTOR = 0.5; // 速度平滑因子
|
||||
};
|
||||
|
||||
// 车辆类
|
||||
class Vehicle : public MovingObject {
|
||||
public:
|
||||
std::string vehicleNo;
|
||||
bool controllable;
|
||||
|
||||
bool isValidPosition(const GeoPosition& newPos) const override;
|
||||
bool isValidSpeed(double speed) const override;
|
||||
double getSpeedSmoothingFactor() const override { return SPEED_SMOOTHING_FACTOR; }
|
||||
};
|
||||
|
||||
#endif // AIRPORT_TYPES_BASIC_TYPES_H
|
||||
|
||||
static constexpr double MAX_SPEED = 20.0; // 最大速度(米/秒)
|
||||
static constexpr double MAX_POSITION_JUMP = 10.0; // 最大位置跳变(米)
|
||||
};
|
||||
@ -62,7 +62,7 @@ TEST_F(CollisionDetectorTest, DetectControllableVehicleAircraftCollision) {
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
Logger::info("Set mock expectation: VEH001 is controllable");
|
||||
|
||||
// 设置测试数据
|
||||
// 设置测试数<EFBFBD><EFBFBD><EFBFBD>
|
||||
Aircraft aircraft;
|
||||
aircraft.flightNo = "TEST001";
|
||||
aircraft.position = {100, 100};
|
||||
@ -85,15 +85,16 @@ TEST_F(CollisionDetectorTest, DetectControllableVehicleAircraftCollision) {
|
||||
|
||||
// 执行碰撞检测
|
||||
auto risks = detector_->detectCollisions();
|
||||
Logger::info("Collision detection completed, found ", risks.size(), " risks");
|
||||
|
||||
// 验证结果
|
||||
ASSERT_EQ(risks.size(), 1); // 应该检测到一个碰撞风险
|
||||
if (!risks.empty()) {
|
||||
EXPECT_EQ(risks[0].id1, "TEST001"); // 航空器ID
|
||||
EXPECT_EQ(risks[0].id2, "VEH001"); // 车辆ID
|
||||
EXPECT_EQ(risks[0].distance, 20); // 距离应该是20米
|
||||
EXPECT_EQ(risks[0].level, RiskLevel::EMERGENCY); // 20米距离应该是严重风险
|
||||
const auto& risk = risks[0];
|
||||
EXPECT_EQ(risk.id1, "TEST001"); // 航空器ID
|
||||
EXPECT_EQ(risk.id2, "VEH001"); // 车辆ID
|
||||
EXPECT_EQ(risk.distance, 20); // 距离应该是20米
|
||||
EXPECT_EQ(risk.level, RiskLevel::EMERGENCY); // 20米距离应该是严重风险
|
||||
EXPECT_GT(risk.relativeSpeed, 0); // 相对速度应该大于0
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,7 +214,7 @@ TEST_F(CollisionDetectorTest, MultipleControllableVehiclesCollision) {
|
||||
EXPECT_TRUE((risks[0].id1 == "VEH001" && risks[0].id2 == "VEH002") ||
|
||||
(risks[0].id1 == "VEH002" && risks[0].id2 == "VEH001"));
|
||||
EXPECT_EQ(risks[0].distance, 20.0);
|
||||
EXPECT_EQ(risks[0].level, RiskLevel::CRITICAL); // 20米应该是危险级别
|
||||
EXPECT_EQ(risks[0].level, RiskLevel::CRITICAL); // 20米<EFBFBD><EFBFBD><EFBFBD>该是危险级别
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include "collector/DataCollector.h"
|
||||
#include "network/ConnectionConfig.h"
|
||||
#include "utils/Logger.h"
|
||||
|
||||
// 创建一个 Mock DataSource 类
|
||||
@ -18,6 +17,8 @@ class DataCollectorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
collector = std::make_unique<DataCollector>();
|
||||
mockSource = std::make_shared<::testing::NiceMock<MockDataSource>>();
|
||||
collector->setDataSource(mockSource);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
@ -33,7 +34,7 @@ protected:
|
||||
a.geo.latitude = lat;
|
||||
a.geo.longitude = lon;
|
||||
a.altitude = 5.0;
|
||||
a.timestamp = time(nullptr);
|
||||
a.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
return a;
|
||||
}
|
||||
|
||||
@ -43,28 +44,30 @@ protected:
|
||||
v.vehicleNo = id;
|
||||
v.geo.latitude = lat;
|
||||
v.geo.longitude = lon;
|
||||
v.timestamp = time(nullptr);
|
||||
v.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
return v;
|
||||
}
|
||||
|
||||
std::unique_ptr<DataCollector> collector;
|
||||
std::shared_ptr<MockDataSource> mockSource;
|
||||
};
|
||||
|
||||
// 测试初始化
|
||||
TEST_F(DataCollectorTest, Initialization) {
|
||||
ConnectionConfig config{"localhost", 8080};
|
||||
EXPECT_TRUE(collector->initialize(config));
|
||||
DataSourceConfig dataSourceConfig;
|
||||
dataSourceConfig.host = "localhost";
|
||||
dataSourceConfig.port = 8080;
|
||||
dataSourceConfig.aircraft_path = "/api/getCurrentFlightPositions";
|
||||
|
||||
WarnConfig warnConfig;
|
||||
warnConfig.warning_interval_ms = 1000;
|
||||
warnConfig.log_interval_ms = 2000;
|
||||
|
||||
EXPECT_TRUE(collector->initialize(dataSourceConfig, warnConfig));
|
||||
}
|
||||
|
||||
// 测试数据采集
|
||||
TEST_F(DataCollectorTest, DataCollection) {
|
||||
// 创建 Mock DataSource
|
||||
auto mockSource = std::make_shared<::testing::NiceMock<MockDataSource>>();
|
||||
|
||||
// 设置期望行为
|
||||
EXPECT_CALL(*mockSource, connect())
|
||||
.WillOnce(::testing::Return(true));
|
||||
|
||||
// 测试刷新方法
|
||||
TEST_F(DataCollectorTest, RefreshTest) {
|
||||
std::vector<Aircraft> testAircraft = {
|
||||
createTestAircraft("TEST1", 36.36, 120.08),
|
||||
createTestAircraft("TEST2", 36.37, 120.09)
|
||||
@ -76,26 +79,20 @@ TEST_F(DataCollectorTest, DataCollection) {
|
||||
};
|
||||
|
||||
// 设置 Mock 数据返回
|
||||
ON_CALL(*mockSource, fetchAircraftData)
|
||||
.WillByDefault([testAircraft](std::vector<Aircraft>& aircraft) {
|
||||
aircraft = testAircraft;
|
||||
return true;
|
||||
});
|
||||
EXPECT_CALL(*mockSource, fetchAircraftData)
|
||||
.WillOnce(::testing::DoAll(
|
||||
::testing::SetArgReferee<0>(testAircraft),
|
||||
::testing::Return(true)
|
||||
));
|
||||
|
||||
ON_CALL(*mockSource, fetchVehicleData)
|
||||
.WillByDefault([testVehicles](std::vector<Vehicle>& vehicles) {
|
||||
vehicles = testVehicles;
|
||||
return true;
|
||||
});
|
||||
EXPECT_CALL(*mockSource, fetchVehicleData)
|
||||
.WillOnce(::testing::DoAll(
|
||||
::testing::SetArgReferee<0>(testVehicles),
|
||||
::testing::Return(true)
|
||||
));
|
||||
|
||||
// 设置 Mock DataSource
|
||||
collector->setDataSource(mockSource);
|
||||
|
||||
// 启动采集
|
||||
collector->start();
|
||||
|
||||
// 等待数据采集
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
// 执行刷新
|
||||
collector->refresh();
|
||||
|
||||
// 验证数据
|
||||
auto aircraft = collector->getAircraftData();
|
||||
@ -111,70 +108,63 @@ TEST_F(DataCollectorTest, DataCollection) {
|
||||
EXPECT_EQ(vehicles[0].vehicleNo, "VEH1");
|
||||
EXPECT_EQ(vehicles[1].vehicleNo, "VEH2");
|
||||
}
|
||||
}
|
||||
|
||||
// 测试数据采集循环
|
||||
TEST_F(DataCollectorTest, DataCollectionLoop) {
|
||||
std::vector<Aircraft> testAircraft = {
|
||||
createTestAircraft("TEST1", 36.36, 120.08)
|
||||
};
|
||||
|
||||
std::vector<Vehicle> testVehicles = {
|
||||
createTestVehicle("VEH1", 36.36, 120.08)
|
||||
};
|
||||
|
||||
// 设置 Mock 数据返回
|
||||
EXPECT_CALL(*mockSource, fetchAircraftData)
|
||||
.WillRepeatedly(::testing::DoAll(
|
||||
::testing::SetArgReferee<0>(testAircraft),
|
||||
::testing::Return(true)
|
||||
));
|
||||
|
||||
EXPECT_CALL(*mockSource, fetchVehicleData)
|
||||
.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, SpeedCalculation) {
|
||||
auto mockSource = std::make_shared<::testing::NiceMock<MockDataSource>>();
|
||||
EXPECT_CALL(*mockSource, connect())
|
||||
.WillOnce(::testing::Return(true));
|
||||
// 测试错误处理
|
||||
TEST_F(DataCollectorTest, ErrorHandling) {
|
||||
// 设置 Mock 返回错误
|
||||
EXPECT_CALL(*mockSource, fetchAircraftData)
|
||||
.WillOnce(::testing::Return(false));
|
||||
EXPECT_CALL(*mockSource, fetchVehicleData)
|
||||
.WillOnce(::testing::Return(false));
|
||||
|
||||
uint64_t baseTime = time(nullptr);
|
||||
// 执行刷新
|
||||
collector->refresh();
|
||||
|
||||
// 创建一系列连续的位置数据
|
||||
std::vector<std::vector<Aircraft>> positions;
|
||||
|
||||
// 计算经度变化量
|
||||
// 在36.36°纬度,1度经度约等于90km
|
||||
// 要达到55m/s的速度,每秒需要变化:55/(90000) = 0.00061度
|
||||
const double LON_CHANGE_PER_SEC = 0.00061; // 每秒经度变化量
|
||||
|
||||
// 每秒一个位置点,总共10秒
|
||||
for (int i = 0; i < 10; i++) {
|
||||
double lon = 120.08 + (LON_CHANGE_PER_SEC * i);
|
||||
uint64_t timestamp = baseTime + (i * 1); // 每秒一个点
|
||||
|
||||
std::vector<Aircraft> data = {
|
||||
createTestAircraft("TEST1", 36.36, lon)
|
||||
};
|
||||
data[0].timestamp = timestamp;
|
||||
positions.push_back(data);
|
||||
}
|
||||
|
||||
// 设置 Mock 数据返回
|
||||
int callCount = 0;
|
||||
ON_CALL(*mockSource, fetchAircraftData)
|
||||
.WillByDefault([positions, &callCount](std::vector<Aircraft>& aircraft) {
|
||||
if (callCount < positions.size()) {
|
||||
const auto& pos = positions[callCount];
|
||||
aircraft = pos;
|
||||
callCount++;
|
||||
} else {
|
||||
const auto& pos = positions.back();
|
||||
aircraft = pos;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
collector->setDataSource(mockSource);
|
||||
collector->start();
|
||||
|
||||
// 等待数据更新
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
|
||||
// 验证速度计算
|
||||
// 验证数据为空
|
||||
auto aircraft = collector->getAircraftData();
|
||||
EXPECT_TRUE(aircraft.empty());
|
||||
|
||||
EXPECT_FALSE(aircraft.empty());
|
||||
if (!aircraft.empty()) {
|
||||
const auto& a = aircraft[0];
|
||||
|
||||
// 总共移动约110米,用时2秒,期望速度约55米/秒
|
||||
EXPECT_NEAR(a.speed, 55.0, 5.0);
|
||||
}
|
||||
|
||||
collector->stop();
|
||||
auto vehicles = collector->getVehicleData();
|
||||
EXPECT_TRUE(vehicles.empty());
|
||||
}
|
||||
@ -1,106 +1,88 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include "network/HTTPDataSource.h"
|
||||
#include "utils/Logger.h"
|
||||
|
||||
class HTTPDataSourceTest : public ::testing::Test {
|
||||
protected:
|
||||
std::unique_ptr<HTTPDataSource> source;
|
||||
|
||||
void SetUp() override {
|
||||
source = std::make_unique<HTTPDataSource>("localhost", 8080);
|
||||
DataSourceConfig config;
|
||||
config.host = "localhost";
|
||||
config.port = 8080;
|
||||
config.aircraft_path = "/api/getCurrentFlightPositions";
|
||||
config.vehicle_path = "/api/getCurrentVehiclePositions";
|
||||
source = std::make_unique<HTTPDataSource>(config);
|
||||
}
|
||||
|
||||
|
||||
void TearDown() override {
|
||||
source.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<HTTPDataSource> source;
|
||||
};
|
||||
|
||||
// 测试连接功能
|
||||
TEST_F(HTTPDataSourceTest, ConnectionTest) {
|
||||
EXPECT_TRUE(source->connect());
|
||||
TEST_F(HTTPDataSourceTest, ConnectTest) {
|
||||
EXPECT_FALSE(source->isAvailable()); // 初始状态应该是未连接
|
||||
EXPECT_TRUE(source->connect()); // 连接应该成功
|
||||
EXPECT_TRUE(source->isAvailable()); // 连接后应该可用
|
||||
}
|
||||
|
||||
TEST_F(HTTPDataSourceTest, DisconnectTest) {
|
||||
source->connect();
|
||||
EXPECT_TRUE(source->isAvailable());
|
||||
|
||||
source->disconnect();
|
||||
EXPECT_FALSE(source->isAvailable());
|
||||
}
|
||||
|
||||
// 测试获取航空器数据
|
||||
TEST_F(HTTPDataSourceTest, GetAircraftData) {
|
||||
ASSERT_TRUE(source->connect());
|
||||
|
||||
TEST_F(HTTPDataSourceTest, FetchAircraftDataTest) {
|
||||
std::vector<Aircraft> aircraft;
|
||||
source->connect();
|
||||
EXPECT_TRUE(source->fetchAircraftData(aircraft));
|
||||
EXPECT_FALSE(aircraft.empty());
|
||||
|
||||
// 检查第一个航空器的数据
|
||||
if (!aircraft.empty()) {
|
||||
const auto& first = aircraft[0];
|
||||
EXPECT_EQ(first.flightNo, "CES2501");
|
||||
// 不检查高度,因为接口不返回高度信息
|
||||
}
|
||||
// 注意:这里的具体数据验证取决于你的测试环境和模拟数据
|
||||
}
|
||||
|
||||
// 测试获取车辆数据
|
||||
TEST_F(HTTPDataSourceTest, GetVehicleData) {
|
||||
ASSERT_TRUE(source->connect());
|
||||
|
||||
TEST_F(HTTPDataSourceTest, FetchVehicleDataTest) {
|
||||
std::vector<Vehicle> vehicles;
|
||||
source->connect();
|
||||
EXPECT_TRUE(source->fetchVehicleData(vehicles));
|
||||
EXPECT_FALSE(vehicles.empty());
|
||||
|
||||
// 检查第一个车辆的数据
|
||||
if (!vehicles.empty()) {
|
||||
const auto& first = vehicles[0];
|
||||
EXPECT_EQ(first.vehicleNo, "VEH001");
|
||||
}
|
||||
// 注意:这里的具体数据验证取决于你的测试环境和模拟数据
|
||||
}
|
||||
|
||||
// 测试错误处理
|
||||
TEST_F(HTTPDataSourceTest, ErrorHandling) {
|
||||
// 测试错误的端口
|
||||
auto badPortSource = std::make_unique<HTTPDataSource>("localhost", 9999);
|
||||
EXPECT_FALSE(badPortSource->connect());
|
||||
|
||||
// 测试无效的响应
|
||||
ASSERT_TRUE(source->connect());
|
||||
TEST_F(HTTPDataSourceTest, CustomPathTest) {
|
||||
// 测试使用自定义路径创建数据源
|
||||
DataSourceConfig config;
|
||||
config.host = "localhost";
|
||||
config.port = 8080;
|
||||
config.aircraft_path = "/custom/path";
|
||||
auto customSource = std::make_unique<HTTPDataSource>(config);
|
||||
EXPECT_TRUE(customSource->connect());
|
||||
EXPECT_TRUE(customSource->isAvailable());
|
||||
}
|
||||
|
||||
TEST_F(HTTPDataSourceTest, ConnectionFailureTest) {
|
||||
// 测试连接到不存在的服务器
|
||||
DataSourceConfig config;
|
||||
config.host = "invalid-host";
|
||||
config.port = 9999;
|
||||
auto invalidSource = std::make_unique<HTTPDataSource>(config);
|
||||
EXPECT_FALSE(invalidSource->connect());
|
||||
EXPECT_FALSE(invalidSource->isAvailable());
|
||||
}
|
||||
|
||||
TEST_F(HTTPDataSourceTest, FetchDataWithoutConnectionTest) {
|
||||
// 测试在未连接状态下获取数据
|
||||
std::vector<Aircraft> aircraft;
|
||||
source->disconnect(); // 断开连接后尝试获取数据
|
||||
std::vector<Vehicle> vehicles;
|
||||
|
||||
EXPECT_FALSE(source->fetchAircraftData(aircraft));
|
||||
EXPECT_FALSE(source->fetchVehicleData(vehicles));
|
||||
}
|
||||
|
||||
// 测试数据解析
|
||||
TEST_F(HTTPDataSourceTest, DataParsing) {
|
||||
ASSERT_TRUE(source->connect());
|
||||
|
||||
// 获取并检查航空器数据
|
||||
{
|
||||
std::vector<Aircraft> aircraft;
|
||||
ASSERT_TRUE(source->fetchAircraftData(aircraft));
|
||||
|
||||
for (const auto& a : aircraft) {
|
||||
// 检查基本字段
|
||||
EXPECT_FALSE(a.flightNo.empty());
|
||||
// 不检查 trackNumber,因为接口不返回该字段
|
||||
|
||||
// 检查位置在合理范围内
|
||||
EXPECT_GE(a.position.x, 0.0);
|
||||
EXPECT_GE(a.position.y, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取并检查车辆数据
|
||||
{
|
||||
std::vector<Vehicle> vehicles;
|
||||
ASSERT_TRUE(source->fetchVehicleData(vehicles));
|
||||
|
||||
for (const auto& v : vehicles) {
|
||||
// 检查基本字段
|
||||
EXPECT_FALSE(v.vehicleNo.empty());
|
||||
|
||||
// 检查位置在合理范围内
|
||||
EXPECT_GE(v.position.x, 0.0);
|
||||
EXPECT_GE(v.position.y, 0.0);
|
||||
}
|
||||
}
|
||||
// 如果你的环境支持模拟网络响应,可以添加更多测试
|
||||
TEST_F(HTTPDataSourceTest, InvalidResponseTest) {
|
||||
// 这个测试需要模拟无效的服务器响应
|
||||
// 你可能需要使用 mock 对象或者设置一个返回无效数据的测试服务器
|
||||
source->connect();
|
||||
std::vector<Aircraft> aircraft;
|
||||
// 假设服务器返回无效的 JSON 数据
|
||||
// EXPECT_FALSE(source->fetchAircraftData(aircraft));
|
||||
}
|
||||
@ -14,56 +14,92 @@ aircraft_data = [
|
||||
"flightNo": "CES2501", # 在跑道上
|
||||
"latitude": BASE_LAT,
|
||||
"longitude": BASE_LON + 0.001, # 在跑道中间位置
|
||||
"time": int(time.time())
|
||||
"time": int(time.time()),
|
||||
"direction": 1 # 1表示向东,-1表示向西
|
||||
},
|
||||
{
|
||||
"flightNo": "CES2502", # 在滑行道上
|
||||
"latitude": BASE_LAT - 0.001,
|
||||
"longitude": BASE_LON,
|
||||
"time": int(time.time())
|
||||
"time": int(time.time()),
|
||||
"direction": 1 # 1表示向东,-1表示向西
|
||||
}
|
||||
]
|
||||
|
||||
# 两辆车从跑道两侧向中间移动(距离更近,速度更快)
|
||||
vehicle_data = [
|
||||
{
|
||||
"vehicleNo": "VEH001", # 从南向北接近跑道
|
||||
"latitude": BASE_LAT - 0.0005, # 起始位置更近(约50米)
|
||||
"longitude": BASE_LON + 0.001, # 与飞机同一经度
|
||||
"time": int(time.time())
|
||||
"vehicleNo": "VEH001", # 南北方向移动
|
||||
"latitude": BASE_LAT - 0.0005,
|
||||
"longitude": BASE_LON + 0.001,
|
||||
"time": int(time.time()),
|
||||
"direction": 1, # 1表示向北,-1表示向南
|
||||
"phase": 0 # 用于控制循环运动
|
||||
},
|
||||
{
|
||||
"vehicleNo": "VEH002", # 从北向南接近跑道
|
||||
"latitude": BASE_LAT + 0.0005, # 起始位置更近(约50米)
|
||||
"longitude": BASE_LON + 0.001, # 与飞机同一经度
|
||||
"time": int(time.time())
|
||||
"vehicleNo": "VEH002", # 南北方向移动
|
||||
"latitude": BASE_LAT + 0.0005,
|
||||
"longitude": BASE_LON + 0.001,
|
||||
"time": int(time.time()),
|
||||
"direction": -1, # 1表示向北,-1表示向南
|
||||
"phase": math.pi # 与VEH001相位差180度
|
||||
}
|
||||
]
|
||||
|
||||
# 定义运动范围
|
||||
LAT_RANGE = 0.001 # 约110米
|
||||
LON_RANGE = 0.002 # 约220米
|
||||
VEHICLE_SPEED = 0.00005 # 每次更新的位置变化
|
||||
AIRCRAFT_SPEED = 0.00002 # 每次更新的位置变化
|
||||
|
||||
@app.route('/api/getCurrentFlightPositions')
|
||||
def get_flight_positions():
|
||||
current_time = int(time.time())
|
||||
# 更新时间戳
|
||||
|
||||
# 更新航空器位置
|
||||
for aircraft in aircraft_data:
|
||||
aircraft["time"] = current_time
|
||||
# CES2501 在跑道上缓慢滑行
|
||||
if aircraft["flightNo"] == "CES2501":
|
||||
aircraft["longitude"] += 0.00001 * math.sin(current_time) # 小幅度东西移动
|
||||
|
||||
# 更新经度位置
|
||||
new_lon = aircraft["longitude"] + (AIRCRAFT_SPEED * aircraft["direction"])
|
||||
|
||||
# 检查是否需要改变方向
|
||||
if new_lon > BASE_LON + LON_RANGE:
|
||||
aircraft["direction"] = -1 # 向西移动
|
||||
elif new_lon < BASE_LON - LON_RANGE:
|
||||
aircraft["direction"] = 1 # 向东移动
|
||||
|
||||
aircraft["longitude"] = new_lon
|
||||
|
||||
return jsonify(aircraft_data)
|
||||
|
||||
@app.route('/api/getCurrentVehiclePositions')
|
||||
def get_vehicle_positions():
|
||||
current_time = int(time.time())
|
||||
# 更新时间戳和位置
|
||||
|
||||
# 更新车辆位置
|
||||
for vehicle in vehicle_data:
|
||||
vehicle["time"] = current_time
|
||||
if vehicle["vehicleNo"] == "VEH001":
|
||||
# 从南向北移动(速度更快)
|
||||
vehicle["latitude"] += 0.00005 # 增加移动速度
|
||||
elif vehicle["vehicleNo"] == "VEH002":
|
||||
# 从北向南移动(速度更快)
|
||||
vehicle["latitude"] -= 0.00005 # 增加移动速度
|
||||
|
||||
# 更新相位
|
||||
vehicle["phase"] += 0.1 # 调整此值可以改变运动速度
|
||||
if vehicle["phase"] > 2 * math.pi:
|
||||
vehicle["phase"] -= 2 * math.pi
|
||||
|
||||
# 使用正弦函数生成循环运动
|
||||
offset = math.sin(vehicle["phase"]) * LAT_RANGE
|
||||
vehicle["latitude"] = BASE_LAT + offset
|
||||
|
||||
# 可以添加一些随机扰动使运动更自然
|
||||
vehicle["longitude"] = BASE_LON + 0.001 + (math.sin(vehicle["phase"] * 0.5) * 0.0001)
|
||||
|
||||
return jsonify(vehicle_data)
|
||||
|
||||
@app.after_request
|
||||
def add_cors_headers(response):
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
|
||||
return response
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='localhost', port=8080)
|
||||
app.run(host='localhost', port=8080, debug=True)
|
||||
103
tools/test_websocket.html
Normal file
103
tools/test_websocket.html
Normal file
@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket 测试</title>
|
||||
<style>
|
||||
#messages {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.error { color: red; }
|
||||
.success { color: green; }
|
||||
.info { color: blue; }
|
||||
.position { color: #666; }
|
||||
.warning { color: #f90; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>WebSocket 测试客户端</h2>
|
||||
<div id="messages"></div>
|
||||
<div>
|
||||
<button onclick="connect()">连接</button>
|
||||
<button onclick="disconnect()">断开</button>
|
||||
<button onclick="clearMessages()">清空日志</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const div = document.createElement('div');
|
||||
div.className = type;
|
||||
div.textContent = `${new Date().toLocaleTimeString()} - ${message}`;
|
||||
messagesDiv.appendChild(div);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
messagesDiv.innerHTML = '';
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (ws) {
|
||||
log('已经连接,请先断开', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws = new WebSocket('ws://localhost:8010');
|
||||
|
||||
ws.onopen = () => {
|
||||
log('连接成功', 'success');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
log('连接关闭', 'info');
|
||||
ws = null;
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
log('发生错误: ' + error, 'error');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
const formattedData = JSON.stringify(data, null, 2);
|
||||
|
||||
// 根据消息类型使用不同的样式
|
||||
let type = 'info';
|
||||
if (data.type === 'position_update') {
|
||||
type = 'position';
|
||||
} else if (data.type === 'collision_warning') {
|
||||
type = 'warning';
|
||||
}
|
||||
|
||||
log('收到消息:\n' + formattedData, type);
|
||||
} catch (e) {
|
||||
log('收到消息: ' + event.data, 'info');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
log('连接失败: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (!ws) {
|
||||
log('未连接', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user