From aade3009e1890eb1ffdf8efd761f1f6104d561b4 Mon Sep 17 00:00:00 2001 From: sladro Date: Wed, 28 Jan 2026 17:36:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=86=E5=A4=87=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + config/system_config.json | 4 +- src/collector/DataCollector.cpp | 20 +- src/config/AirportBounds.cpp | 16 +- src/config/AirportBounds.h | 11 +- src/core/System.cpp | 59 +++-- src/core/System.h | 7 + src/detector/CollisionDetector.cpp | 19 +- src/mock/MockDataService.cpp | 1 - src/network/ConfigHttpServer.cpp | 361 +++++++++++++++++++++++++++ src/network/ConfigHttpServer.h | 50 ++++ src/network/HTTPClient.cpp | 28 ++- src/network/HTTPClient.h | 2 +- src/network/WebSocketServer.cpp | 72 ++++-- src/network/WebSocketServer.h | 3 +- src/utils/Logger.h | 33 ++- src/vehicle/ControllableVehicles.cpp | 101 ++++---- src/vehicle/ControllableVehicles.h | 32 ++- 命令.md | 27 ++ 账号信息_20251107.docx | Bin 0 -> 13271 bytes 20 files changed, 690 insertions(+), 157 deletions(-) create mode 100644 src/network/ConfigHttpServer.cpp create mode 100644 src/network/ConfigHttpServer.h create mode 100644 命令.md create mode 100644 账号信息_20251107.docx diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d4dd67..3f0193f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ set(LIB_SOURCES src/network/HTTPClient.cpp src/network/TrafficLightHttpServer.cpp src/network/TrafficLightTcpServer.cpp + src/network/ConfigHttpServer.cpp src/spatial/CoordinateConverter.cpp src/types/BasicTypes.cpp src/types/VehicleData.cpp diff --git a/config/system_config.json b/config/system_config.json index f9ae417..efcebd9 100644 --- a/config/system_config.json +++ b/config/system_config.json @@ -92,8 +92,8 @@ "read_timeout_ms": 2000 }, "unmanned_vehicle": { - "host": "localhost", - "port": 8081, + "host": "10.232.18.23", + "port": 8020, "location_path": "/api/VehicleLocationInfo", "status_path": "/api/VehicleStateInfo", "command_path": "/api/VehicleCommandInfo", diff --git a/src/collector/DataCollector.cpp b/src/collector/DataCollector.cpp index c5fef5b..e2549a2 100644 --- a/src/collector/DataCollector.cpp +++ b/src/collector/DataCollector.cpp @@ -389,19 +389,15 @@ bool DataCollector::fetchUnmannedVehicleData() { bool any_success = false; bool any_failure = false; - auto& vehicles = ControllableVehicles::getInstance().getVehicles(); + auto controllables = ControllableVehicles::getInstance().getControllableVehicleIdsSnapshot(); - 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); - } + for (const auto& vehicle_id : controllables) { + std::string status; + if (dataSource_->fetchUnmannedVehicleStatus(vehicle_id, status)) { + any_success = true; + } else { + any_failure = true; + Logger::error("获取无人车状态失败, vehicleNo: " + vehicle_id); } } diff --git a/src/config/AirportBounds.cpp b/src/config/AirportBounds.cpp index 2e45a1e..14b4514 100644 --- a/src/config/AirportBounds.cpp +++ b/src/config/AirportBounds.cpp @@ -152,7 +152,8 @@ AreaType AirportBounds::getAreaType(const Vector2D& position) const { return AreaType::TEST_ZONE; } -const AreaConfig& AirportBounds::getAreaConfig(AreaType type) const { +AreaConfig AirportBounds::getAreaConfig(AreaType type) const { + std::shared_lock lock(config_mutex_); auto it = areaConfigs_.find(type); if (it == areaConfigs_.end()) { throw std::runtime_error("Invalid area type"); @@ -160,6 +161,19 @@ const AreaConfig& AirportBounds::getAreaConfig(AreaType type) const { return it->second; } +bool AirportBounds::setWarningZoneAircraftRadius(AreaType type, double radius) { + if (!(radius > 0.0) || !std::isfinite(radius)) { + return false; + } + std::unique_lock lock(config_mutex_); + auto it = areaConfigs_.find(type); + if (it == areaConfigs_.end()) { + return false; + } + it->second.warning_zone_radius.aircraft = radius; + return true; +} + bool AirportBounds::isPointInBounds(const Vector2D& position) const { // 在机场坐标系中判断是否在边界内 bool result = airportBounds_.contains(position); diff --git a/src/config/AirportBounds.h b/src/config/AirportBounds.h index 55d212a..d03e6eb 100644 --- a/src/config/AirportBounds.h +++ b/src/config/AirportBounds.h @@ -7,6 +7,8 @@ #include "spatial/QuadTree.h" #include "AreaConfig.h" #include +#include +#include // 机场区域定义 class AirportBounds { @@ -18,7 +20,11 @@ public: virtual AreaType getAreaType(const Vector2D& position) const; // 获取区域配置 - virtual const AreaConfig& getAreaConfig(AreaType type) const; + virtual AreaConfig getAreaConfig(AreaType type) const; + + // 动态更新区域配置(运行中生效) + // 例如:修改 warning_zone_radius.aircraft(你提到的 200) + virtual bool setWarningZoneAircraftRadius(AreaType type, double radius); // 获取整个机场边界 virtual const Bounds& getAirportBounds() const { return airportBounds_; } @@ -47,6 +53,9 @@ protected: Bounds airportBounds_; // 整个机场边界 std::unordered_map areaBounds_; // 各区域边界 std::unordered_map areaConfigs_; // 各区域配置 + + // 保护 areaConfigs_ 的并发读写(HTTP 动态修改 vs 处理线程读取) + mutable std::shared_mutex config_mutex_; // 从配置文件加载数据 virtual void loadConfig(const std::string& configFile); diff --git a/src/core/System.cpp b/src/core/System.cpp index d46fd16..0b102c9 100644 --- a/src/core/System.cpp +++ b/src/core/System.cpp @@ -14,14 +14,15 @@ System* System::instance_ = nullptr; +namespace { +volatile sig_atomic_t g_system_signal_requested = 0; +} + System::System() : controllableVehicles_(ControllableVehicles::getInstance()) { instance_ = this; - std::signal(SIGINT, signalHandler); - std::signal(SIGTERM, signalHandler); - - // 使用单例模式获取配置实例 - auto& config = SystemConfig::instance(); + // 信号处理由 main 负责注册(避免覆盖 main 的 handler,且 signal handler 内不可做非异步信号安全操作) + (void)SystemConfig::instance(); } System::~System() { @@ -30,11 +31,8 @@ System::~System() { } void System::signalHandler(int signal) { - Logger::info("Received signal: ", signal); - if (instance_) { - instance_->stop(); - } - std::exit(0); + // async-signal-safe: 仅记录请求,不做锁/线程/join/IO/exit 等操作 + g_system_signal_requested = signal; } bool System::initialize() { @@ -56,6 +54,14 @@ bool System::initialize() { ws_thread_ = std::thread([this]() { ws_server_->start(); }); Logger::info("WebSocket server initialized on port ", system_config.websocket.port); + // 前端动态配置接口(HTTP) + // POST http://:8090/config/runway/warning_zone_radius/aircraft {"value": 300} + config_http_server_ = std::make_unique( + static_cast(8090), + 50, + *this); + config_http_server_->start(1); + // 红绿灯改为 TCP 被动接收(对齐 tcp_server.py:accept -> recv -> 响应 -> close) // 这里先写死监听端口 8082(0.0.0.0),用于你先在测试平台验证链路 traffic_light_tcp_server_ = std::make_unique( @@ -167,6 +173,11 @@ void System::stop() { traffic_light_tcp_server_->stop(); } + // Stop Config HTTP server + if (config_http_server_) { + config_http_server_->stop(); + } + // 保留旧 HTTP server 成员(当前不启用) if (traffic_light_http_server_) { traffic_light_http_server_->stop(); @@ -183,6 +194,21 @@ void System::stop() { Logger::info("System fully stopped."); } +bool System::setRunwayWarningZoneAircraftRadius(double radius, double* oldValue) { + if (!airportBounds_) { + return false; + } + try { + auto cfg = airportBounds_->getAreaConfig(AreaType::RUNWAY); + if (oldValue) { + *oldValue = cfg.warning_zone_radius.aircraft; + } + } catch (...) { + return false; + } + return airportBounds_->setWarningZoneAircraftRadius(AreaType::RUNWAY, radius); +} + void System::processLoop() { while (running_) { try { @@ -220,11 +246,9 @@ void System::processLoop() { 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"; - } + bool controllable = controllableVehicles_.isControllable(veh.vehicleNo); + veh.type = controllable ? MovingObjectType::UNMANNED : MovingObjectType::SPECIAL; + veh.isControllable = controllable; objects.push_back(&veh); } @@ -299,8 +323,11 @@ void System::checkUnmannedVehicleSafetyZones( std::unordered_map& vehicleMaxRiskLevels, std::vector& detectedRisks) { - // 遍历所有无人车 + // 遍历所有无人车(可控车辆) for (const auto& vehicle : vehicles) { + if (!controllableVehicles_.isControllable(vehicle.id)) { + continue; + } // 遍历所有路口安全区 for (const auto& [intersectionId, zone] : safetyZones_) { if (zone->getState() == SafetyZoneState::INACTIVE) { diff --git a/src/core/System.h b/src/core/System.h index 046841c..a8e4d31 100644 --- a/src/core/System.h +++ b/src/core/System.h @@ -13,6 +13,7 @@ #include "config/AirportBounds.h" #include "vehicle/ControllableVehicles.h" #include "network/WebSocketServer.h" +#include "network/ConfigHttpServer.h" #include "network/MessageTypes.h" #include "config/IntersectionConfig.h" #include "network/TrafficLightHttpServer.h" @@ -41,6 +42,9 @@ public: void broadcastCollisionWarning(const CollisionRisk& risk); void broadcastVehicleCommand(const VehicleCommand& cmd); void broadcastTrafficLightStatus(const TrafficLightSignal& signal); + + // 运行中动态修改配置:对应 airport_bounds.json 里 runway.warning_zone_radius.aircraft(默认 200) + bool setRunwayWarningZoneAircraftRadius(double radius, double* oldValue = nullptr); const SystemConfig& getSystemConfig() const { return SystemConfig::instance(); } const IntersectionConfig& getIntersectionConfig() const { return intersection_config_; } @@ -95,6 +99,9 @@ private: // WebSocket 服务器 std::unique_ptr ws_server_; std::thread ws_thread_; + + // 前端动态配置 HTTP Server + std::unique_ptr config_http_server_; // 新增: 红绿灯 HTTP 服务器 std::unique_ptr traffic_light_http_server_; diff --git a/src/detector/CollisionDetector.cpp b/src/detector/CollisionDetector.cpp index f938cb5..a25887b 100644 --- a/src/detector/CollisionDetector.cpp +++ b/src/detector/CollisionDetector.cpp @@ -33,22 +33,11 @@ void CollisionDetector::updateTraffic(const std::vector& aircraft, 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; - } + bool controllable = controllableVehicles_ && controllableVehicles_->isControllable(vehicle.vehicleNo); + updatedVehicle.type = controllable ? MovingObjectType::UNMANNED : MovingObjectType::SPECIAL; + updatedVehicle.isControllable = controllable; // 插入四叉树 try { diff --git a/src/mock/MockDataService.cpp b/src/mock/MockDataService.cpp index fe31d76..c8cf7d2 100644 --- a/src/mock/MockDataService.cpp +++ b/src/mock/MockDataService.cpp @@ -74,7 +74,6 @@ std::vector MockDataService::generateVehicleData() { } 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, diff --git a/src/network/ConfigHttpServer.cpp b/src/network/ConfigHttpServer.cpp new file mode 100644 index 0000000..8e82051 --- /dev/null +++ b/src/network/ConfigHttpServer.cpp @@ -0,0 +1,361 @@ +#include "ConfigHttpServer.h" + +#include "core/System.h" +#include "utils/Logger.h" +#include "vehicle/ControllableVehicles.h" + +#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; + +namespace { + +http::response json_response(const http::request& req, + http::status status, + const json& body) { + http::response res{status, 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() = body.dump(); + res.prepare_payload(); + return res; +} + +http::response method_not_allowed(const http::request& req, + const char* allow) { + 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, "application/json"); + res.set(http::field::allow, allow); + res.keep_alive(req.keep_alive()); + res.body() = json{{"status", "error"}, {"message", "Method Not Allowed"}}.dump(); + res.prepare_payload(); + return res; +} + +} // namespace + +class ConfigSession : public std::enable_shared_from_this { + tcp::socket socket_; + beast::flat_buffer buffer_; + http::request req_; + net::strand strand_; + System& system_; + +public: + explicit ConfigSession(tcp::socket&& socket, net::io_context& ioc, System& system_ref) + : socket_(std::move(socket)), strand_(ioc.get_executor()), 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) { + boost::ignore_unused(bytes); + self->on_read(ec); + })); + } + + void on_read(beast::error_code ec) { + if (ec == http::error::end_of_stream) { + return do_close(); + } + if (ec) { + Logger::error("ConfigSession read error: ", ec.message()); + return; + } + handle_request(); + } + + void handle_request() { + if (req_.method() != http::verb::post) { + return send_response(method_not_allowed(req_, "POST")); + } + + // Endpoint A: 前端车辆注册表(增量更新) + // POST /api/VehicleRegistry + // Body: [{"vehicleID":"A001","vehicleType":"WUREN"}, ...] + if (req_.target() == "/api/VehicleRegistry") { + try { + json body = json::parse(req_.body()); + if (!body.is_array()) { + return send_response(json_response(req_, http::status::bad_request, + json{{"status", "error"}, {"message", "Body must be an array"}})); + } + + std::vector entries; + entries.reserve(body.size()); + + std::unordered_map type_counts; + std::vector errors; + + auto is_allowed_type = [](const std::string& t) { + return t == "WUREN" || t == "TEQIN" || t == "HANGKONG" || t == "PUTONG" || t == "JIUYUAN"; + }; + + for (size_t i = 0; i < body.size(); ++i) { + const auto& item = body.at(i); + if (!item.is_object()) { + errors.push_back("index " + std::to_string(i) + ": item must be an object"); + continue; + } + if (!item.contains("vehicleID") || !item.contains("vehicleType")) { + errors.push_back("index " + std::to_string(i) + ": missing vehicleID/vehicleType"); + continue; + } + if (!item.at("vehicleID").is_string() || !item.at("vehicleType").is_string()) { + errors.push_back("index " + std::to_string(i) + ": vehicleID/vehicleType must be string"); + continue; + } + std::string vehicleID = item.at("vehicleID").get(); + std::string vehicleType = item.at("vehicleType").get(); + if (vehicleID.empty()) { + errors.push_back("index " + std::to_string(i) + ": vehicleID must be non-empty"); + continue; + } + if (!is_allowed_type(vehicleType)) { + errors.push_back("index " + std::to_string(i) + ": invalid vehicleType=" + vehicleType); + continue; + } + + entries.push_back(VehicleRegistryEntry{vehicleID, vehicleType}); + type_counts[vehicleType] += 1; + } + + if (!errors.empty()) { + return send_response(json_response(req_, http::status::bad_request, + json{{"status", "error"}, {"message", "Invalid request"}, {"errors", errors}})); + } + + auto& registry = ControllableVehicles::getInstance(); + registry.updateRegistry(entries); + + auto controllables = registry.getControllableVehicleIdsSnapshot(); + std::vector controllable_ids; + controllable_ids.reserve(controllables.size()); + for (const auto& id : controllables) { + controllable_ids.push_back(id); + } + + auto now_ms = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + + return send_response(json_response(req_, http::status::ok, + json{{"status", "success"}, + {"updatedAt", now_ms}, + {"updated", static_cast(entries.size())}, + {"controllableCount", static_cast(controllable_ids.size())}, + {"typesCount", type_counts}, + {"controllableVehicleIDs", controllable_ids}})); + } catch (const json::parse_error& e) { + return send_response(json_response(req_, http::status::bad_request, + json{{"status", "error"}, {"message", "Invalid JSON"}, {"detail", e.what()}})); + } catch (const std::exception& e) { + return send_response(json_response(req_, http::status::internal_server_error, + json{{"status", "error"}, {"message", "Internal error"}, {"detail", e.what()}})); + } + } + + // Endpoint B: 运行中动态修改配置 + // POST /config/runway/warning_zone_radius/aircraft + // Body: {"value": 300} + if (req_.target() != "/config/runway/warning_zone_radius/aircraft") { + return send_response(json_response(req_, http::status::not_found, + json{{"status", "error"}, {"message", "Not Found"}})); + } + + try { + json body = json::parse(req_.body()); + if (!body.contains("value")) { + return send_response(json_response(req_, http::status::bad_request, + json{{"status", "error"}, {"message", "Missing field: value"}})); + } + + double value = body.at("value").get(); + double oldValue = 0.0; + if (!system_.setRunwayWarningZoneAircraftRadius(value, &oldValue)) { + return send_response(json_response(req_, http::status::internal_server_error, + json{{"status", "error"}, {"message", "Failed to update config"}})); + } + + Logger::info("Updated runway warning_zone_radius.aircraft: ", oldValue, " -> ", value); + + return send_response(json_response(req_, http::status::ok, + json{{"status", "success"}, + {"area", "runway"}, + {"field", "warning_zone_radius.aircraft"}, + {"old", oldValue}, + {"new", value}})); + } catch (const json::parse_error& e) { + return send_response(json_response(req_, http::status::bad_request, + json{{"status", "error"}, {"message", "Invalid JSON"}, {"detail", e.what()}})); + } catch (const std::exception& e) { + return send_response(json_response(req_, http::status::internal_server_error, + json{{"status", "error"}, {"message", "Internal error"}, {"detail", e.what()}})); + } + } + + 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) { + boost::ignore_unused(bytes); + self->on_write(sp->need_eof(), ec); + })); + } + + void on_write(bool close, beast::error_code ec) { + if (ec) { + Logger::error("ConfigSession write error: ", ec.message()); + return; + } + if (close) { + return do_close(); + } + do_read(); + } + + void do_close() { + beast::error_code ec; + socket_.shutdown(tcp::socket::shutdown_send, ec); + } +}; + +class ConfigListener : public std::enable_shared_from_this { + net::io_context& ioc_; + tcp::acceptor acceptor_; + net::strand strand_; + int max_connections_; + System& system_; + +public: + ConfigListener(net::io_context& ioc, tcp::endpoint endpoint, int max_connections, System& system_ref) + : ioc_(ioc) + , acceptor_(ioc) + , strand_(ioc.get_executor()) + , max_connections_(max_connections) + , system_(system_ref) { + beast::error_code ec; + acceptor_.open(endpoint.protocol(), ec); + if (ec) throw beast::system_error{ec}; + + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) throw beast::system_error{ec}; + + acceptor_.bind(endpoint, ec); + if (ec) throw beast::system_error{ec}; + + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) throw beast::system_error{ec}; + } + + void run() { do_accept(); } + + void stop() { + beast::error_code ec; + acceptor_.close(ec); + } + +private: + void do_accept() { + acceptor_.async_accept( + net::bind_executor(strand_, + [self = shared_from_this()](beast::error_code ec, tcp::socket socket) { + self->on_accept(ec, std::move(socket)); + })); + } + + void on_accept(beast::error_code ec, tcp::socket socket) { + if (ec) { + if (ec != net::error::operation_aborted) { + Logger::error("ConfigListener accept error: ", ec.message()); + } + return; + } + + std::make_shared(std::move(socket), ioc_, system_)->run(); + do_accept(); + } +}; + +ConfigHttpServer::ConfigHttpServer(uint16_t port, int max_connections, System& system_ref) + : port_(port), max_connections_(max_connections), ioc_(1), system_(system_ref) {} + +ConfigHttpServer::~ConfigHttpServer() { + if (running_.load()) { + stop(); + } +} + +void ConfigHttpServer::start(int num_threads) { + if (running_.exchange(true)) { + return; + } + + if (num_threads < 1) num_threads = 1; + + auto const address = net::ip::make_address("0.0.0.0"); + try { + listener_ = std::make_shared( + ioc_, tcp::endpoint{address, port_}, max_connections_, system_); + listener_->run(); + + Logger::info("ConfigHttpServer listening on ", address.to_string(), ":", port_); + + threads_.reserve(num_threads); + for (int i = 0; i < num_threads; ++i) { + threads_.emplace_back([this] { run_ioc(); }); + } + } catch (const std::exception& e) { + running_ = false; + Logger::error("Failed to start ConfigHttpServer: ", e.what()); + throw; + } +} + +void ConfigHttpServer::stop() { + if (!running_.exchange(false)) { + return; + } + + Logger::info("Stopping ConfigHttpServer..."); + net::post(ioc_, [this]() { + if (listener_) { + listener_->stop(); + listener_.reset(); + } + }); + ioc_.stop(); + for (auto& t : threads_) { + if (t.joinable()) t.join(); + } + threads_.clear(); + Logger::info("ConfigHttpServer stopped."); +} + +void ConfigHttpServer::run_ioc() { + try { + ioc_.run(); + } catch (const std::exception& e) { + Logger::error("Exception in ConfigHttpServer I/O thread: ", e.what()); + } +} + +} // namespace network diff --git a/src/network/ConfigHttpServer.h b/src/network/ConfigHttpServer.h new file mode 100644 index 0000000..90fdad1 --- /dev/null +++ b/src/network/ConfigHttpServer.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class System; + +namespace network { + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; + +class ConfigListener; + +// 提供一个极简配置更新 HTTP 接口(给前端调用) +class ConfigHttpServer { + uint16_t port_; + int max_connections_; + net::io_context ioc_; + std::shared_ptr listener_; + std::vector threads_; + std::atomic running_{false}; + System& system_; + +public: + ConfigHttpServer(uint16_t port, int max_connections, System& system_ref); + ~ConfigHttpServer(); + + ConfigHttpServer(const ConfigHttpServer&) = delete; + ConfigHttpServer& operator=(const ConfigHttpServer&) = delete; + + void start(int num_threads = 1); + void stop(); + +private: + void run_ioc(); +}; + +} // namespace network diff --git a/src/network/HTTPClient.cpp b/src/network/HTTPClient.cpp index a1408f0..bf8ad03 100644 --- a/src/network/HTTPClient.cpp +++ b/src/network/HTTPClient.cpp @@ -39,7 +39,7 @@ size_t HTTPClient::WriteCallback(void* contents, size_t size, size_t nmemb, void return total_size; } -bool HTTPClient::sendCommand(const std::string& ip, int port, const VehicleCommand& command) const { +bool HTTPClient::sendCommand(const std::string& host, int port, const std::string& command_path, const VehicleCommand& command) const { if (!curl_) { Logger::error("CURL not initialized"); return false; @@ -47,7 +47,11 @@ bool HTTPClient::sendCommand(const std::string& ip, int port, const VehicleComma // 构造请求URL std::stringstream url; - url << "http://" << ip << ":" << port << "/api/VehicleCommandInfo"; + url << "http://" << host << ":" << port; + if (!command_path.empty() && command_path.front() != '/') { + url << '/'; + } + url << command_path; // 生成消息唯一 id std::stringstream transId; @@ -80,15 +84,21 @@ bool HTTPClient::sendCommand(const std::string& ip, int port, const VehicleComma } }()}, {"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} + {"longitude", command.longitude} }; + if (command.type == CommandType::SIGNAL) { + request["signalState"] = getSignalStateString(command.signalState); + request["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 request_body = request.dump(); response_buffer_.clear(); diff --git a/src/network/HTTPClient.h b/src/network/HTTPClient.h index 998fa74..49da7ef 100644 --- a/src/network/HTTPClient.h +++ b/src/network/HTTPClient.h @@ -11,7 +11,7 @@ public: ~HTTPClient(); // 发送控制指令 - bool sendCommand(const std::string& ip, int port, const VehicleCommand& command) const; + bool sendCommand(const std::string& host, int port, const std::string& command_path, const VehicleCommand& command) const; private: static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp); diff --git a/src/network/WebSocketServer.cpp b/src/network/WebSocketServer.cpp index a4a26aa..cde39da 100644 --- a/src/network/WebSocketServer.cpp +++ b/src/network/WebSocketServer.cpp @@ -1,6 +1,10 @@ #include +#include #include #include +#include +#include +#include #include "network/WebSocketServer.h" #include "utils/Logger.h" #include @@ -15,7 +19,9 @@ namespace network { std::lock_guard lock(sessions_mutex_); for (auto& session : sessions_) { try { - session.lock()->close(boost::beast::websocket::close_code::normal); + if (auto ws = session.lock()) { + ws->close(boost::beast::websocket::close_code::normal); + } } catch (...) { // 忽略关闭时的错误 } @@ -27,35 +33,55 @@ namespace network { } void WebSocketServer::start() { + running_.store(true, std::memory_order_relaxed); 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); // 移除已失效的会话 - } + if (!running_.load(std::memory_order_relaxed)) { + return; } - Logger::debug("广播消息完成: ", successCount, " 个成功, ", failCount, " 个失败, 当前共 ", sessions_.size(), " 个连接"); + auto msg = std::make_shared(message); + + auto do_broadcast = [this, msg]() { + 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()) { + try { + session->write(boost::asio::buffer(*msg)); + ++successCount; + ++it; + } catch (...) { + Logger::warning("广播消息到客户端失败"); + ++failCount; + it = sessions_.erase(it); + } + } else { + Logger::debug("移除失效的会话"); + it = sessions_.erase(it); + } + } + + Logger::debug("广播消息完成: ", successCount, " 个成功, ", failCount, " 个失败, 当前共 ", sessions_.size(), " 个连接"); + }; + + // 保持原语义:broadcast 返回前完成实际发送,但确保所有 websocket 操作在 io_context 线程执行 + auto done = std::make_shared>(); + auto fut = done->get_future(); + + // dispatch: 若已在 strand 线程内会直接执行,避免等待导致自锁;否则投递到 io_context 线程执行 + boost::asio::dispatch(strand_, [do_broadcast, done]() mutable { + do_broadcast(); + done->set_value(); + }); + fut.wait(); } void WebSocketServer::handleAccept() { @@ -118,7 +144,7 @@ namespace network { } void WebSocketServer::stop() { - running_ = false; + running_.store(false, std::memory_order_relaxed); Logger::info("正在停止 WebSocket 服务器..."); ioc_.stop(); // 停止 io_context diff --git a/src/network/WebSocketServer.h b/src/network/WebSocketServer.h index 426be69..011a77c 100644 --- a/src/network/WebSocketServer.h +++ b/src/network/WebSocketServer.h @@ -23,10 +23,11 @@ private: void doRead(std::shared_ptr> ws); boost::asio::io_context ioc_; + boost::asio::io_context::strand strand_{ioc_}; boost::asio::ip::tcp::acceptor acceptor_; std::vector>> sessions_; std::mutex sessions_mutex_; - std::atomic running_{true}; + std::atomic running_{false}; }; } // namespace network \ No newline at end of file diff --git a/src/utils/Logger.h b/src/utils/Logger.h index ac045cb..d1d0d58 100644 --- a/src/utils/Logger.h +++ b/src/utils/Logger.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include enum class LogLevel { DEBUG, @@ -19,46 +21,45 @@ class Logger { public: static void initialize(const std::string& filename, LogLevel level = LogLevel::INFO) { std::lock_guard lock(getMutex()); - currentLevel() = level; + currentLevel().store(level, std::memory_order_relaxed); logFile().open(filename, std::ios::app); } static void setLogLevel(LogLevel level) { - std::lock_guard lock(getMutex()); - currentLevel() = level; + currentLevel().store(level, std::memory_order_relaxed); } template static void debug(Args... args) { - if (currentLevel() <= LogLevel::DEBUG) { + if (currentLevel().load(std::memory_order_relaxed) <= LogLevel::DEBUG) { log("DEBUG", args...); } } template static void info(Args... args) { - if (currentLevel() <= LogLevel::INFO) { + if (currentLevel().load(std::memory_order_relaxed) <= LogLevel::INFO) { log("INFO", args...); } } template static void warning(Args... args) { - if (currentLevel() <= LogLevel::WARNING) { + if (currentLevel().load(std::memory_order_relaxed) <= LogLevel::WARNING) { log("WARNING", args...); } } template static void error(Args... args) { - if (currentLevel() <= LogLevel::ERROR) { + if (currentLevel().load(std::memory_order_relaxed) <= LogLevel::ERROR) { log("ERROR", args...); } } private: - static LogLevel& currentLevel() { - static LogLevel level = LogLevel::INFO; + static std::atomic& currentLevel() { + static std::atomic level{ LogLevel::INFO }; return level; } @@ -71,6 +72,16 @@ private: static std::mutex mutex; return mutex; } + + static std::tm safeLocalTime(std::time_t t) { + std::tm tm{}; +#if defined(_WIN32) + localtime_s(&tm, &t); +#else + localtime_r(&t, &tm); +#endif + return tm; + } template static void log(const char* level, Args... args) { @@ -78,9 +89,11 @@ private: auto now_c = std::chrono::system_clock::to_time_t(now); auto now_ms = std::chrono::duration_cast( now.time_since_epoch()) % 1000; + + const std::tm tm = safeLocalTime(now_c); std::stringstream ss; - ss << std::put_time(std::localtime(&now_c), "%Y-%m-%d %H:%M:%S") + ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << '.' << std::setfill('0') << std::setw(3) << now_ms.count() << " [" << level << "] "; diff --git a/src/vehicle/ControllableVehicles.cpp b/src/vehicle/ControllableVehicles.cpp index 2e1ec17..01959fd 100644 --- a/src/vehicle/ControllableVehicles.cpp +++ b/src/vehicle/ControllableVehicles.cpp @@ -1,23 +1,21 @@ #include "ControllableVehicles.h" #include "utils/Logger.h" -#include #include +#include +#include "config/SystemConfig.h" // 定义静态成员变量 ControllableVehicles* ControllableVehicles::instance_ = nullptr; ControllableVehicles& ControllableVehicles::getInstance() { if (instance_ == nullptr) { - instance_ = new ControllableVehicles("config/unmanned_vehicles.json"); + instance_ = new ControllableVehicles(); } return *instance_; } -ControllableVehicles::ControllableVehicles(const std::string& configFile) +ControllableVehicles::ControllableVehicles() : http_client_(std::make_unique()) { - if (!configFile.empty()) { - loadConfig(configFile); - } } ControllableVehicles::~ControllableVehicles() { @@ -27,67 +25,62 @@ ControllableVehicles::~ControllableVehicles() { } } -const std::vector& ControllableVehicles::getVehicles() const { - return vehicles_; +void ControllableVehicles::updateRegistry(const std::vector& entries) { + if (entries.empty()) { + return; + } + + int applied = 0; + { + std::unique_lock lock(mutex_); + for (const auto& e : entries) { + if (e.vehicleID.empty()) { + continue; + } + vehicle_type_by_id_[e.vehicleID] = e.vehicleType; + + if (e.vehicleType == "WUREN") { + controllable_vehicle_ids_.insert(e.vehicleID); + } else { + controllable_vehicle_ids_.erase(e.vehicleID); + } + applied++; + } + } + + Logger::info("Vehicle registry updated. applied=", applied, + " total=", vehicle_type_by_id_.size(), + " controllable(WUREN)=", controllable_vehicle_ids_.size()); } -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; - }); +std::optional ControllableVehicles::getVehicleType(const std::string& vehicleID) const { + std::shared_lock lock(mutex_); + auto it = vehicle_type_by_id_.find(vehicleID); + if (it == vehicle_type_by_id_.end()) { + return std::nullopt; + } + return it->second; +} - return iter != vehicles_.end() ? &(*iter) : nullptr; +std::unordered_set ControllableVehicles::getControllableVehicleIdsSnapshot() const { + std::shared_lock lock(mutex_); + return controllable_vehicle_ids_; } 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; - } + std::shared_lock lock(mutex_); + return controllable_vehicle_ids_.find(vehicleNo) != controllable_vehicle_ids_.end(); } 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"); + if (!isControllable(vehicleNo)) { + Logger::debug("Skip sendCommand: vehicle not controllable: ", vehicleNo); return false; } try { - bool success = http_client_->sendCommand(vehicle->ip, vehicle->port, command); + const auto& cfg = SystemConfig::instance().data_source.vehicle; + bool success = http_client_->sendCommand(cfg.host, cfg.port, cfg.command_path, command); if (success) { Logger::info("Successfully sent command to vehicle ", vehicleNo, ": ", command.type == CommandType::SIGNAL ? "SIGNAL" : diff --git a/src/vehicle/ControllableVehicles.h b/src/vehicle/ControllableVehicles.h index d789bbd..64040d8 100644 --- a/src/vehicle/ControllableVehicles.h +++ b/src/vehicle/ControllableVehicles.h @@ -1,34 +1,44 @@ #pragma once #include -#include #include +#include +#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; // 端口号 +struct VehicleRegistryEntry { + std::string vehicleID; + std::string vehicleType; // WUREN/TEQIN/HANGKONG/PUTONG/JIUYUAN }; class ControllableVehicles { private: static ControllableVehicles* instance_; - ControllableVehicles(const std::string& configFile); + ControllableVehicles(); ControllableVehicles(const ControllableVehicles&) = delete; ControllableVehicles& operator=(const ControllableVehicles&) = delete; ~ControllableVehicles(); - std::vector vehicles_; + // 运行时车辆类型注册表(由前端接口增量更新) + mutable std::shared_mutex mutex_; + std::unordered_map vehicle_type_by_id_; + std::unordered_set controllable_vehicle_ids_; // vehicleType == WUREN + 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; + + // 增量更新车辆注册表(仅更新 entries 中出现的 vehicleID),并同步可控车辆集合(WUREN) + void updateRegistry(const std::vector& entries); + + // 查询 + std::optional getVehicleType(const std::string& vehicleID) const; + std::unordered_set getControllableVehicleIdsSnapshot() 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/命令.md b/命令.md new file mode 100644 index 0000000..298d08c --- /dev/null +++ b/命令.md @@ -0,0 +1,27 @@ + + +rm -rf build + + export PATH=/opt/rh/devtoolset-9/root/usr/bin:$PATH + export LD_LIBRARY_PATH=/opt/rh/devtoolset-9/root/usr/lib64:$LD_LIBRARY_PATH + + /opt/cmake/bin/cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + -DLINK_STATIC_DEPS=ON \ + -DFULL_STATIC=OFF \ + -DBoost_NO_SYSTEM_PATHS=ON \ + -DBOOST_ROOT=/opt/boost-1.69 \ + -DBOOST_LIBRARYDIR=/opt/boost-1.69/lib + + /opt/cmake/bin/cmake --build build -j + + + + + +ldd /opt/collision_avoidance/bin/collision_avoidancenew | grep "not found" || true + + + +docker run --rm -it --network host -v D:/App/C++/CollisionAvoidance:/src -w /src centos:7 bash + +docker commit --pause=false 5f4a906adb1b qdairporttestbackend:20260128 \ No newline at end of file diff --git a/账号信息_20251107.docx b/账号信息_20251107.docx new file mode 100644 index 0000000000000000000000000000000000000000..02de1a8043aa1a15f4bb781959dd008b4f709fc8 GIT binary patch literal 13271 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 literal 0 HcmV?d00001