diff --git a/.gitignore b/.gitignore index d7379af..06aea89 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ hls_streams/live *.db *.log .runner -capture/ +captures/ # 特例列表 !rknn_sdk/librknn_api/aarch64/*.so diff --git a/captures/20260107_083403_id20.jpg b/captures/20260107_083403_id20.jpg deleted file mode 100644 index 4a7cee4..0000000 Binary files a/captures/20260107_083403_id20.jpg and /dev/null differ diff --git a/captures/20260107_083403_id7.jpg b/captures/20260107_083403_id7.jpg deleted file mode 100644 index 925bf2e..0000000 Binary files a/captures/20260107_083403_id7.jpg and /dev/null differ diff --git a/captures/20260107_083404_id19.jpg b/captures/20260107_083404_id19.jpg deleted file mode 100644 index 69510cc..0000000 Binary files a/captures/20260107_083404_id19.jpg and /dev/null differ diff --git a/captures/20260107_083405_id27.jpg b/captures/20260107_083405_id27.jpg deleted file mode 100644 index c0b5239..0000000 Binary files a/captures/20260107_083405_id27.jpg and /dev/null differ diff --git a/captures/20260107_083405_id31.jpg b/captures/20260107_083405_id31.jpg deleted file mode 100644 index ce65e18..0000000 Binary files a/captures/20260107_083405_id31.jpg and /dev/null differ diff --git a/captures/20260107_083407_id38.jpg b/captures/20260107_083407_id38.jpg deleted file mode 100644 index 2f5e2b9..0000000 Binary files a/captures/20260107_083407_id38.jpg and /dev/null differ diff --git a/captures/20260107_083410_id45.jpg b/captures/20260107_083410_id45.jpg deleted file mode 100644 index 59eabc2..0000000 Binary files a/captures/20260107_083410_id45.jpg and /dev/null differ diff --git a/captures/20260107_083411_id50.jpg b/captures/20260107_083411_id50.jpg deleted file mode 100644 index e065233..0000000 Binary files a/captures/20260107_083411_id50.jpg and /dev/null differ diff --git a/captures/20260107_083412_id54.jpg b/captures/20260107_083412_id54.jpg deleted file mode 100644 index b68d49c..0000000 Binary files a/captures/20260107_083412_id54.jpg and /dev/null differ diff --git a/captures/20260107_083413_id60.jpg b/captures/20260107_083413_id60.jpg deleted file mode 100644 index 10ec780..0000000 Binary files a/captures/20260107_083413_id60.jpg and /dev/null differ diff --git a/captures/20260107_083414_id52.jpg b/captures/20260107_083414_id52.jpg deleted file mode 100644 index ef18880..0000000 Binary files a/captures/20260107_083414_id52.jpg and /dev/null differ diff --git a/captures/20260107_083416_id58.jpg b/captures/20260107_083416_id58.jpg deleted file mode 100644 index dbd2a7f..0000000 Binary files a/captures/20260107_083416_id58.jpg and /dev/null differ diff --git a/src/config/config_manager.cc b/src/config/config_manager.cc index 770496c..473e19f 100644 --- a/src/config/config_manager.cc +++ b/src/config/config_manager.cc @@ -266,4 +266,29 @@ TripwireConfig ConfigManager::getTripwireConfig() { } return config; +} +bool ConfigManager::updateTripwireLine(float p1_x, float p1_y, float p2_x, float p2_y) { + std::unique_lock lock(m_mutex); // 获取写锁,确保线程安全 + + // 1. 确保 tripwire 对象存在 + if (!m_config_json.contains("tripwire") || !m_config_json["tripwire"].is_object()) { + m_config_json["tripwire"] = json::object(); + } + + // 2. 确保 line 对象存在 + if (!m_config_json["tripwire"].contains("line") || + !m_config_json["tripwire"]["line"].is_object()) { + m_config_json["tripwire"]["line"] = json::object(); + } + + // 3. 更新坐标值 + // 注意:JSON 结构根据 config.json 文件构建 + m_config_json["tripwire"]["line"]["p1"] = {{"x", p1_x}, {"y", p1_y}}; + m_config_json["tripwire"]["line"]["p2"] = {{"x", p2_x}, {"y", p2_y}}; + + spdlog::info("Updating Tripwire: P1({:.2f}, {:.2f}), P2({:.2f}, {:.2f})", p1_x, p1_y, p2_x, + p2_y); + + // 4. 保存到文件 (save_unlocked 内部不加锁,适合在这里调用) + return save_unlocked(); } \ No newline at end of file diff --git a/src/config/config_manager.h b/src/config/config_manager.h index c2093f7..72ba12d 100644 --- a/src/config/config_manager.h +++ b/src/config/config_manager.h @@ -89,6 +89,8 @@ public: TripwireConfig getTripwireConfig(); + bool updateTripwireLine(float p1_x, float p1_y, float p2_x, float p2_y); + private: ConfigManager() = default; ~ConfigManager() = default; diff --git a/src/videoService/video_pipeline.cpp b/src/videoService/video_pipeline.cpp index 11a2435..3cda195 100644 --- a/src/videoService/video_pipeline.cpp +++ b/src/videoService/video_pipeline.cpp @@ -25,20 +25,29 @@ static cv::Point getBottomCenter(const cv::Rect& box) { VideoPipeline::VideoPipeline() : running_(false), next_track_id_(0) { detector_ = std::make_unique(); - // 模型路径 if (detector_->init("../models/vehicle_model.rknn") != 0) { spdlog::error("Failed to initialize YoloDetector"); } else { spdlog::info("YoloDetector initialized successfully."); } + + // === [新增] 注册配置热重载回调 === + // 当 "tripwire" 键发生变化时,ConfigManager 会调用这个 lambda + ConfigManager::getInstance().monitorKey("tripwire", [this](const json& /*new_val*/) { + spdlog::info("Hot-Reload: Tripwire config detected change!"); + + TripwireConfig newConfig = ConfigManager::getInstance().getTripwireConfig(); + + this->setTripwire(newConfig); + }); + + // 初始加载 try { TripwireConfig config = ConfigManager::getInstance().getTripwireConfig(); setTripwire(config); - spdlog::info("Tripwire loaded via ConfigManager: {}, Enabled: {}", config.name, - config.enabled); - + spdlog::info("Tripwire loaded: {}, Enabled: {}", config.name, config.enabled); } catch (const std::exception& e) { - spdlog::warn("Failed to load tripwire config via manager: {}", e.what()); + spdlog::warn("Failed to load tripwire config: {}", e.what()); } } @@ -70,12 +79,16 @@ void VideoPipeline::Stop() { } void VideoPipeline::setTripwire(const TripwireConfig& config) { - // 可以在运行时更新配置 + // [新增] 加锁,防止与 updateTracker 或 drawOverlay 冲突 + std::lock_guard lock(config_mtx_); + tripwire_config_ = config; + // 重置宽高,强制下一帧重新计算像素坐标 current_frame_width_ = 0; current_frame_height_ = 0; - spdlog::info("Tripwire config set: {}", config.name); + + spdlog::info("Tripwire config updated safely: {}", config.name); } void VideoPipeline::inferenceWorker() { @@ -156,9 +169,10 @@ bool VideoPipeline::isLineCrossed(const cv::Point& A, const cv::Point& B, const } // [业务逻辑] 处理跨线车辆 (异步截图入库) -void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat& frame) { +void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat& frame, + const std::string& locationName) { // 启动分离线程,避免阻塞主视频流 - std::thread([this, vehicle, frame]() { + std::thread([this, vehicle, frame, locationName]() { // === 1. 准备目录与文件名 === std::string saveDir = "../captures"; try { @@ -224,8 +238,7 @@ void VideoPipeline::processCrossing(const TrackedVehicle& vehicle, const cv::Mat // 3.2 插入业务主表 // 假设 SystemID 为 1,实际项目可能需配置 int64_t systemId = 1; - std::string location = - tripwire_config_.name.empty() ? "Unkown_Line" : tripwire_config_.name; + std::string location = locationName.empty() ? "Unkown_Line" : locationName; int64_t dataId = deviceDao.ReportIdentification(systemId, location, cColor, cType); @@ -297,20 +310,20 @@ void VideoPipeline::updateTracker(const FrameData& frameData) { float current_is_ev = (det.class_id == 1) ? 1.0f : 0.0f; if (best_match_id != -1) { - // === 匹配成功 === TrackedVehicle& track = tracks_[best_match_id]; - // [新增] 跨线检测 if (tripwire_config_.enabled && track.prev_bottom_center.x != -1) { - bool crossed = isLineCrossed(tripwire_p1_pixel_, tripwire_p2_pixel_, // 线 - track.prev_bottom_center, current_bottom // 轨迹 - ); + bool crossed = isLineCrossed(tripwire_p1_pixel_, tripwire_p2_pixel_, + track.prev_bottom_center, current_bottom); if (crossed) { spdlog::info(">>> Vehicle {} Crossed Line: {} <<<", track.id, tripwire_config_.name); - // 触发业务逻辑 - processCrossing(track, frame); + + // [修改] 传入 tripwire_config_.name 的副本 + // 因为 processCrossing 是异步的,this->tripwire_config_ + // 可能会在线程运行期间被修改 + processCrossing(track, frame, tripwire_config_.name); } } @@ -351,6 +364,7 @@ void VideoPipeline::updateTracker(const FrameData& frameData) { } void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector& trackedObjects) { + std::lock_guard lock(config_mtx_); // 1. 画绊线 (如果启用) if (tripwire_config_.enabled) { cv::line(frame, tripwire_p1_pixel_, tripwire_p2_pixel_, cv::Scalar(0, 255, 255), 2); diff --git a/src/videoService/video_pipeline.hpp b/src/videoService/video_pipeline.hpp index 3b59190..76b2360 100644 --- a/src/videoService/video_pipeline.hpp +++ b/src/videoService/video_pipeline.hpp @@ -40,7 +40,8 @@ private: bool isLineCrossed(const cv::Point& p1, const cv::Point& p2, const cv::Point& line_start, const cv::Point& line_end); - void processCrossing(const TrackedVehicle& vehicle, const cv::Mat& frame); + void processCrossing(const TrackedVehicle& vehicle, const cv::Mat& frame, + const std::string& locationName); private: std::atomic running_; @@ -66,6 +67,7 @@ private: std::mutex output_mtx_; std::condition_variable output_cv_; + std::mutex config_mtx_; TripwireConfig tripwire_config_; // 配置数据 cv::Point tripwire_p1_pixel_; // 缓存:转换后的起点像素坐标 cv::Point tripwire_p2_pixel_; // 缓存:转换后的终点像素坐标 diff --git a/src/web/web_server.cc b/src/web/web_server.cc index d048773..96cc5f9 100644 --- a/src/web/web_server.cc +++ b/src/web/web_server.cc @@ -3,688 +3,107 @@ #include #include +#include "DTOs/common_types.hpp" #include "config/config_manager.h" #include "nlohmann/json.hpp" #include "spdlog/spdlog.h" -WebServer::WebServer(SystemMonitor::SystemMonitor &monitor, - DeviceManager &deviceManager, LiveDataCache &liveDataCache, - AlarmService &alarm_service, uint16_t port) - : crow::Crow(), m_monitor(monitor), - m_device_manager(deviceManager), m_live_data_cache(liveDataCache), - m_alarm_service(alarm_service), m_port(port) { - auto &cors = this->get_middleware(); - cors.global() - .origin("*") - .headers("Content-Type", "Authorization") - .methods("GET"_method, "POST"_method, "OPTIONS"_method); +WebServer::WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager, + LiveDataCache& liveDataCache, AlarmService& alarm_service, uint16_t port) + : crow::Crow(), + m_monitor(monitor), + m_device_manager(deviceManager), + m_live_data_cache(liveDataCache), + m_alarm_service(alarm_service), + m_port(port) { + auto& cors = this->get_middleware(); + cors.global() + .origin("*") + .headers("Content-Type", "Authorization") + .methods("GET"_method, "POST"_method, "OPTIONS"_method); - this->loglevel(crow::LogLevel::Warning); - setup_routes(); + this->loglevel(crow::LogLevel::Warning); + setup_routes(); } -WebServer::~WebServer() { stop(); } +WebServer::~WebServer() { + stop(); +} void WebServer::start() { - if (m_thread.joinable()) { - spdlog::warn("Web server is already running."); - return; - } - m_thread = std::thread([this]() { - spdlog::info("Starting Web server on port {}", m_port); - this->bindaddr("0.0.0.0").port(m_port).run(); - spdlog::info("Web server has stopped."); - }); + if (m_thread.joinable()) { + spdlog::warn("Web server is already running."); + return; + } + m_thread = std::thread([this]() { + spdlog::info("Starting Web server on port {}", m_port); + this->bindaddr("0.0.0.0").port(m_port).run(); + spdlog::info("Web server has stopped."); + }); } void WebServer::stop() { - crow::Crow::stop(); - if (m_thread.joinable()) { - m_thread.join(); - } + crow::Crow::stop(); + if (m_thread.joinable()) { + m_thread.join(); + } } void WebServer::set_shutdown_handler(std::function handler) { - m_shutdown_handler = handler; + m_shutdown_handler = handler; } - -bool WebServer::validate_video_config(const std::string &json_string, - std::string &error_message) { - try { - auto config = nlohmann::json::parse(json_string); - - if (!config.is_object()) { - error_message = "Root is not an object."; - return false; - } - - // 1. 验证 'video_service' - if (!config.contains("video_service") || - !config["video_service"].is_object()) { - error_message = "Missing 'video_service' object."; - return false; - } - if (!config["video_service"].contains("enabled") || - !config["video_service"]["enabled"].is_boolean()) { - error_message = "'video_service' must have 'enabled' (boolean)."; - return false; - } - - // 2. 验证 'video_streams' - if (!config.contains("video_streams") || - !config["video_streams"].is_array()) { - error_message = "Missing 'video_streams' array."; - return false; - } - - // 3. 验证 'video_streams' 中的每个元素 - for (const auto &stream : config["video_streams"]) { - if (!stream.is_object()) { - error_message = "Item in 'video_streams' is not an object."; - return false; - } - // 检查必需的键 - for (const char *key : {"id", "input_url", "module_type"}) { - if (!stream.contains(key) || !stream[key].is_string() || - stream[key].get().empty()) { - error_message = - "Stream missing or invalid key: '" + std::string(key) + "'."; - return false; - } - } - if (!stream.contains("enabled") || !stream["enabled"].is_boolean()) { - error_message = "Stream missing or invalid key: 'enabled' (boolean)."; - return false; - } - if (!stream.contains("module_config") || - !stream["module_config"].is_object()) { - error_message = "Stream missing 'module_config' (object)."; - return false; - } - - // 4. 验证 'module_config' 的关键字段 - const auto &mod_cfg = stream["module_config"]; - for (const char *key : {"model_path", "label_path"}) { - if (!mod_cfg.contains(key) || !mod_cfg[key].is_string() || - mod_cfg[key].get().empty()) { - error_message = - "module_config missing or empty key: '" + std::string(key) + "'."; - return false; - } - } - if (!mod_cfg.contains("class_num") || - !mod_cfg["class_num"].is_number_integer() || - mod_cfg["class_num"].get() <= 0) { - error_message = "module_config missing or invalid 'class_num' (must be " - "integer > 0)."; - return false; - } - if (!mod_cfg.contains("rknn_thread_num") || - !mod_cfg["rknn_thread_num"].is_number_integer()) { - error_message = "module_config missing 'rknn_thread_num' (integer)."; - return false; - } - } - - return true; // 所有检查通过 - - } catch (const nlohmann::json::parse_error &e) { - error_message = "Invalid JSON: " + std::string(e.what()); - return false; - } catch (const std::exception &e) { - error_message = "Validation error: " + std::string(e.what()); - return false; - } -} - -bool WebServer::validate_main_config(const std::string &json_string, - std::string &error_message) { - try { - auto config = nlohmann::json::parse(json_string); - - if (!config.is_object()) { - error_message = "Root is not an object."; - return false; - } - - // 1. 必需的字符串类型 - for (const char *key : - {"device_id", "config_base_path", "mqtt_broker", - "mqtt_client_id_prefix", "data_storage_db_path", "data_cache_db_path", - "log_level", "alarm_rules_path", "piper_executable_path", - "piper_model_path", "video_config_path"}) { - if (!config.contains(key) || !config[key].is_string() || - config[key].get().empty()) { - error_message = - "Missing or invalid/empty string key: '" + std::string(key) + "'."; - return false; - } - } - - // 2. 必需的整数类型 - if (!config.contains("web_server_port") || - !config["web_server_port"].is_number_integer()) { - error_message = "Missing or invalid 'web_server_port' (must be integer)."; - return false; - } - - // 3. 必需的数组类型 - if (!config.contains("tcp_server_ports") || - !config["tcp_server_ports"].is_array()) { - error_message = "Missing or invalid 'tcp_server_ports' (must be array)."; - return false; - } - for (const auto &port : config["tcp_server_ports"]) { - if (!port.is_number_integer()) { - error_message = "Item in 'tcp_server_ports' is not an integer."; - return false; - } - } - - return true; // 所有检查通过 - - } catch (const nlohmann::json::parse_error &e) { - error_message = "Invalid JSON: " + std::string(e.what()); - return false; - } catch (const std::exception &e) { - error_message = "Validation error: " + std::string(e.what()); - return false; - } -} - void WebServer::setup_routes() { - - CROW_ROUTE((*this), "/api/system/id").methods("GET"_method)([this] { - auto deviceID = ConfigManager::getInstance().getDeviceID(); - crow::json::wvalue response; - response["deviceID"] = deviceID; - return response; - }); - - CROW_ROUTE((*this), "/api/system/status").methods("GET"_method)([this] { - auto cpu_util = m_monitor.getCpuUtilization(); - auto mem_info = m_monitor.getMemoryInfo(); - - crow::json::wvalue response; - response["cpu_usage_percentage"] = cpu_util.totalUsagePercentage; - response["memory_total_kb"] = mem_info.total_kb; - response["memory_free_kb"] = mem_info.available_kb; - response["memory_usage_percentage"] = - (mem_info.total_kb > 0) - ? (1.0 - - static_cast(mem_info.available_kb) / mem_info.total_kb) * - 100.0 - : 0.0; - - return response; - }); - - CROW_ROUTE((*this), "/api/devices").methods("GET"_method)([this] { - auto devices_info = m_device_manager.get_all_device_info(); - - std::vector devices_json; - for (const auto &info : devices_info) { - crow::json::wvalue device_obj; - - device_obj["id"] = info.id; - device_obj["type"] = info.type; - device_obj["is_running"] = info.is_running; - - crow::json::wvalue details_obj; - for (const auto &pair : info.connection_details) { - details_obj[pair.first] = pair.second; - } - device_obj["connection_details"] = std::move(details_obj); - - devices_json.push_back(std::move(device_obj)); - } - auto res = crow::response(crow::json::wvalue(devices_json)); - res.set_header("Content-Type", "application/json"); - return res; - }); - - CROW_ROUTE((*this), "/api/data/latest").methods("GET"_method)([this] { - auto latest_data_map = m_live_data_cache.get_all_data(); - - crow::json::wvalue response; - for (const auto &pair : latest_data_map) { - response[pair.first] = crow::json::load(pair.second); - } - auto res = crow::response(response); - res.set_header("Content-Type", "application/json"); - return res; - }); - - CROW_ROUTE((*this), "/api/alarms/active").methods("GET"_method)([this] { - try { - auto json_string = m_alarm_service.getActiveAlarmsJson().dump(); - - auto res = crow::response(200, json_string); - res.set_header("Content-Type", "application/json"); - return res; - - } catch (const std::exception &e) { - spdlog::error("Error processing /api/alarms/active: {}", e.what()); - crow::json::wvalue error_resp; - error_resp["error"] = "Failed to retrieve active alarms."; - - auto res = crow::response(500, error_resp.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - CROW_ROUTE((*this), "/api/alarms/history") - .methods("GET"_method)([this](const crow::request &req) { - int limit = 100; - if (req.url_params.get("limit")) { - try { - limit = std::stoi(req.url_params.get("limit")); - } catch (const std::exception &) { /* ignore invalid */ - } - } - if (limit <= 0) - limit = 100; - - try { - auto json_string = m_alarm_service.getAlarmHistoryJson(limit).dump(); - - auto res = crow::response(200, json_string); - res.set_header("Content-Type", "application/json"); - return res; - - } catch (const std::exception &e) { - spdlog::error("Error processing /api/alarms/history: {}", e.what()); - crow::json::wvalue error_resp; - error_resp["error"] = "Failed to retrieve alarm history."; - - auto res = crow::response(500, error_resp.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - CROW_ROUTE((*this), "/api/alarms/reload").methods("POST"_method)([this]() { - spdlog::info("Web API: Received request to reload alarm rules..."); - - bool success = m_alarm_service.reload_rules(); - - if (success) { - crow::json::wvalue response_json; - response_json["status"] = "success"; - response_json["message"] = "Alarm rules reloaded successfully."; - - auto res = crow::response(200, response_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - - } else { - crow::json::wvalue error_json; - error_json["status"] = "error"; - error_json["message"] = - "Failed to reload alarm rules. Check service logs for details."; - auto res = crow::response(500, error_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - CROW_ROUTE((*this), "/api/alarms/clear") - .methods("POST"_method)([this](const crow::request &req) { - crow::json::rvalue j_body; - try { - j_body = crow::json::load(req.body); - } catch (const std::exception &e) { - spdlog::warn("Failed to parse request body for /api/alarms/clear: {}", - e.what()); - auto res = crow::response(400, "{\"error\":\"Invalid JSON body.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - if (!j_body.has("rule_id") || !j_body.has("device_id")) { - auto res = crow::response( - 400, "{\"error\":\"Missing 'rule_id' or 'device_id'.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - std::string rule_id = j_body["rule_id"].s(); - std::string device_id = j_body["device_id"].s(); - - bool success = m_alarm_service.manually_clear_alarm(rule_id, device_id); - - if (success) { - auto res = crow::response( - 200, "{\"status\":\"success\", \"message\":\"Alarm cleared.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } else { - auto res = crow::response( - 404, "{\"status\":\"error\", \"message\":\"Alarm not " - "found or not active.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - CROW_ROUTE((*this), "/api/alarms/config").methods("GET"_method)([this]() { - std::string rules_json_string = m_alarm_service.get_rules_as_json_string(); - auto res = crow::response(200, rules_json_string); - res.set_header("Content-Type", "application/json"); - return res; - }); - - CROW_ROUTE((*this), "/api/alarms/config") - .methods("POST"_method)([this](const crow::request &req) { - const std::string &new_rules_content = req.body; - - bool success = - m_alarm_service.save_rules_from_json_string(new_rules_content); - - if (success) { - crow::json::wvalue response_json; - response_json["status"] = "success"; - response_json["message"] = - "Rules saved successfully. A reload is required to apply."; - - auto res = crow::response(200, response_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - - } else { - crow::json::wvalue error_json; - error_json["status"] = "error"; - error_json["message"] = - "Failed to save rules. Invalid JSON format or server error."; - - auto res = crow::response(400, error_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - /** - * @brief GET /api/devices/config - * 获取设备配置 (devices.json) 的原始 JSON 字符串 - */ - CROW_ROUTE((*this), "/api/devices/config").methods("GET"_method)([this]() { - std::string rules_json_string = - m_device_manager.get_config_as_json_string(); - auto res = crow::response(200, rules_json_string); - res.set_header("Content-Type", "application/json"); - return res; - }); - - /** - * @brief POST /api/devices/config - * 保存设备配置 (devices.json) 的原始 JSON 字符串 - */ - CROW_ROUTE((*this), "/api/devices/config") - .methods("POST"_method)([this](const crow::request &req) { - const std::string &new_rules_content = req.body; - - // save_config_from_json_string 内部包含 JSON 格式和 Schema 校验 - bool success = - m_device_manager.save_config_from_json_string(new_rules_content); - - if (success) { - crow::json::wvalue response_json; - response_json["status"] = "success"; - response_json["message"] = "Device config saved successfully. A " - "reload is required to apply."; - - auto res = crow::response(200, response_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - - } else { - crow::json::wvalue error_json; - error_json["status"] = "error"; - error_json["message"] = "Failed to save rules. Invalid JSON format " - "or schema. Check service logs."; - - auto res = crow::response(400, error_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - /** - * @brief POST /api/devices/reload - * 通知后端从磁盘重载 devices.json 并应用 - */ - CROW_ROUTE((*this), "/api/devices/reload").methods("POST"_method)([this]() { - spdlog::info("Web API: Received request to reload device rules..."); - - bool success = m_device_manager.reload_config_from_file(); - - if (success) { - crow::json::wvalue response_json; - response_json["status"] = "success"; - response_json["message"] = "Device rules reload posted successfully."; - - auto res = crow::response(200, response_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - - } else { - crow::json::wvalue error_json; - error_json["status"] = "error"; - error_json["message"] = - "Failed to post device rules reload. Check service logs."; - auto res = crow::response(500, error_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - }); - - /** - * @brief GET /api/video_config - * 获取 video_config.json 的原始 JSON 字符串 - */ - CROW_ROUTE((*this), "/api/video_config").methods("GET"_method)([this]() { - std::string config_path; - try { - - config_path = ConfigManager::getInstance().getVideoConfigPath(); - } catch (const std::exception &e) { - spdlog::error("Failed to get video config path from ConfigManager: {}", - e.what()); - auto res = crow::response(500, "{\"error\":\"Server configuration error: " - "cannot determine config path.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - std::ifstream ifs(config_path); - if (!ifs.is_open()) { - spdlog::error("Failed to open video config file for reading: {}", - config_path); - auto res = - crow::response(404, "{\"error\":\"Video config file not found.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - std::string content((std::istreambuf_iterator(ifs)), - (std::istreambuf_iterator())); - - auto res = crow::response(200, content); - res.set_header("Content-Type", "application/json"); - return res; - }); - - /** - * @brief POST /api/video_config - * 验证并保存 video_config.json - */ - CROW_ROUTE((*this), "/api/video_config") - .methods("POST"_method)([this](const crow::request &req) { - const std::string &new_config_content = req.body; - std::string error_msg; - - if (!validate_video_config(new_config_content, error_msg)) { - spdlog::warn("Web API: Failed to save video_config: {}", error_msg); - crow::json::wvalue error_json; - error_json["status"] = "error"; - error_json["message"] = "Invalid JSON format or schema: " + error_msg; - auto res = crow::response(400, error_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - - std::string config_path = - ConfigManager::getInstance().getVideoConfigPath(); - - std::ofstream ofs(config_path); - if (!ofs.is_open()) { - spdlog::error("Failed to open video config file for writing: {}", - config_path); - auto res = crow::response( - 500, "{\"error\":\"Failed to write config file on server.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - auto json_data = nlohmann::json::parse(new_config_content); - ofs << json_data.dump(4); // 格式化写入 - ofs.close(); - - // 4. 成功响应 - spdlog::info("Video config successfully updated via API at {}", - config_path); - crow::json::wvalue response_json; - response_json["status"] = "success"; - response_json["message"] = - "Video config saved. Send POST to /api/service/reload to apply."; - auto res = crow::response(200, response_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - }); - - CROW_ROUTE((*this), "/api/service/reload").methods("POST"_method)([this]() { - if (m_shutdown_handler) { - spdlog::info("Web API: Received request to reload service..."); - m_shutdown_handler(); - - auto res = - crow::response(202, "{\"status\":\"restarting\", " - "\"message\":\"Service is restarting...\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - spdlog::error("Web API: /api/service/reload called, but shutdown handler " - "is not set!"); - auto res = crow::response( - 500, "{\"error\":\"Shutdown handler not configured on server.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - }); - - - /** - * @brief GET /api/config - * 获取主 config.json 的原始 JSON 字符串 - */ - CROW_ROUTE((*this), "/api/config").methods("GET"_method)([this]() { - std::string config_path; - try { - // 1. 从 ConfigManager 获取路径 - config_path = ConfigManager::getInstance().getConfigFilePath(); - if (config_path.empty()) { - throw std::runtime_error("ConfigManager returned an empty path."); - } - } catch (const std::exception &e) { - spdlog::error("Failed to get main config path from ConfigManager: {}", - e.what()); - auto res = crow::response(500, "{\"error\":\"Server configuration error: " - "cannot determine config path.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - // 2. 读取文件 - std::ifstream ifs(config_path); - if (!ifs.is_open()) { - spdlog::error("Failed to open main config file for reading: {}", - config_path); - auto res = - crow::response(404, "{\"error\":\"Main config file not found.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - // 3. 返回内容 - std::string content((std::istreambuf_iterator(ifs)), - (std::istreambuf_iterator())); - - auto res = crow::response(200, content); - res.set_header("Content-Type", "application/json"); - return res; - }); - - /** - * @brief POST /api/config - * 验证并保存主 config.json - */ - CROW_ROUTE((*this), "/api/config") - .methods("POST"_method)([this](const crow::request &req) { - const std::string &new_config_content = req.body; - std::string error_msg; - - if (!validate_main_config(new_config_content, error_msg)) { - spdlog::warn("Web API: Failed to save main config: {}", error_msg); - crow::json::wvalue error_json; - error_json["status"] = "error"; - error_json["message"] = "Invalid JSON format or schema: " + error_msg; - auto res = crow::response(400, error_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - } - - std::string config_path = - ConfigManager::getInstance().getConfigFilePath(); - if (config_path.empty()) { - auto res = crow::response( - 500, "{\"error\":\"Failed to get config file path on server.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - std::ofstream ofs(config_path); - if (!ofs.is_open()) { - spdlog::error("Failed to open main config file for writing: {}", - config_path); - auto res = crow::response( - 500, "{\"error\":\"Failed to write config file on server.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - try { - auto json_data = nlohmann::json::parse(new_config_content); - ofs << json_data.dump(4); - ofs.close(); - } catch (const std::exception &e) { - spdlog::error("Failed to re-parse and dump main config: {}", - e.what()); - auto res = crow::response( - 500, "{\"error\":\"Failed to serialize config for writing.\"}"); - res.set_header("Content-Type", "application/json"); - return res; - } - - spdlog::info("Main config successfully updated via API at {}", - config_path); - crow::json::wvalue response_json; - response_json["status"] = "success"; - response_json["message"] = - "Main config saved. A full service restart (e.g., via " - "/api/service/reload) is required to apply changes."; - auto res = crow::response(200, response_json.dump()); - res.set_header("Content-Type", "application/json"); - return res; - }); + CROW_ROUTE((*this), "/api/v1/video/setTripwire") + .methods("POST"_method)([](const crow::request& req) { + // 1. 解析请求体为 JSON + auto json_body = crow::json::load(req.body); + + // 2. 校验 JSON 格式是否合法 + if (!json_body) { + return crow::response(400, "Invalid JSON format"); + } + + // 3. 校验必要字段是否存在 + if (!json_body.has("x1") || !json_body.has("y1") || !json_body.has("x2") || + !json_body.has("y2")) { + return crow::response(400, "Missing coordinates (x1, y1, x2, y2)"); + } + + try { + // 4. 提取数据 (假设前端传的是 0-100 的数值) + // 使用 .d() 获取 double 类型 + double x1_pct = json_body["x1"].d(); + double y1_pct = json_body["y1"].d(); + double x2_pct = json_body["x2"].d(); + double y2_pct = json_body["y2"].d(); + + // 5. 简单的范围校验 (可选,防止异常数据) + if (x1_pct < 0 || x1_pct > 100 || y1_pct < 0 || y1_pct > 100 || x2_pct < 0 || + x2_pct > 100 || y2_pct < 0 || y2_pct > 100) { + return crow::response(400, "Values must be between 0 and 100"); + } + + // 6. 归一化:除以 100 转换为 0.0 - 1.0 + float p1_x = static_cast(x1_pct / 100.0); + float p1_y = static_cast(y1_pct / 100.0); + float p2_x = static_cast(x2_pct / 100.0); + float p2_y = static_cast(y2_pct / 100.0); + + // 7. 调用 ConfigManager 更新并保存 + bool success = + ConfigManager::getInstance().updateTripwireLine(p1_x, p1_y, p2_x, p2_y); + + if (success) { + json response_json = {{"status", "success"}, + {"message", "Tripwire updated successfully"}}; + return crow::response(200, response_json.dump()); + } else { + return crow::response(500, "Failed to save configuration"); + } + + } catch (const std::exception& e) { + spdlog::error("Error parsing tripwire coordinates: {}", e.what()); + return crow::response(400, "Invalid data types"); + } + }); } \ No newline at end of file diff --git a/src/web/web_server.cc.bak b/src/web/web_server.cc.bak new file mode 100644 index 0000000..7891e82 --- /dev/null +++ b/src/web/web_server.cc.bak @@ -0,0 +1,690 @@ +#include "web_server.h" + +#include +#include + +#include "config/config_manager.h" +#include "nlohmann/json.hpp" +#include "spdlog/spdlog.h" + +WebServer::WebServer(SystemMonitor::SystemMonitor &monitor, + DeviceManager &deviceManager, LiveDataCache &liveDataCache, + AlarmService &alarm_service, uint16_t port) + : crow::Crow(), m_monitor(monitor), + m_device_manager(deviceManager), m_live_data_cache(liveDataCache), + m_alarm_service(alarm_service), m_port(port) { + auto &cors = this->get_middleware(); + cors.global() + .origin("*") + .headers("Content-Type", "Authorization") + .methods("GET"_method, "POST"_method, "OPTIONS"_method); + + this->loglevel(crow::LogLevel::Warning); + setup_routes(); +} + +WebServer::~WebServer() { stop(); } + +void WebServer::start() { + if (m_thread.joinable()) { + spdlog::warn("Web server is already running."); + return; + } + m_thread = std::thread([this]() { + spdlog::info("Starting Web server on port {}", m_port); + this->bindaddr("0.0.0.0").port(m_port).run(); + spdlog::info("Web server has stopped."); + }); +} + +void WebServer::stop() { + crow::Crow::stop(); + if (m_thread.joinable()) { + m_thread.join(); + } +} + +void WebServer::set_shutdown_handler(std::function handler) { + m_shutdown_handler = handler; +} + +bool WebServer::validate_video_config(const std::string &json_string, + std::string &error_message) { + try { + auto config = nlohmann::json::parse(json_string); + + if (!config.is_object()) { + error_message = "Root is not an object."; + return false; + } + + // 1. 验证 'video_service' + if (!config.contains("video_service") || + !config["video_service"].is_object()) { + error_message = "Missing 'video_service' object."; + return false; + } + if (!config["video_service"].contains("enabled") || + !config["video_service"]["enabled"].is_boolean()) { + error_message = "'video_service' must have 'enabled' (boolean)."; + return false; + } + + // 2. 验证 'video_streams' + if (!config.contains("video_streams") || + !config["video_streams"].is_array()) { + error_message = "Missing 'video_streams' array."; + return false; + } + + // 3. 验证 'video_streams' 中的每个元素 + for (const auto &stream : config["video_streams"]) { + if (!stream.is_object()) { + error_message = "Item in 'video_streams' is not an object."; + return false; + } + // 检查必需的键 + for (const char *key : {"id", "input_url", "module_type"}) { + if (!stream.contains(key) || !stream[key].is_string() || + stream[key].get().empty()) { + error_message = + "Stream missing or invalid key: '" + std::string(key) + "'."; + return false; + } + } + if (!stream.contains("enabled") || !stream["enabled"].is_boolean()) { + error_message = "Stream missing or invalid key: 'enabled' (boolean)."; + return false; + } + if (!stream.contains("module_config") || + !stream["module_config"].is_object()) { + error_message = "Stream missing 'module_config' (object)."; + return false; + } + + // 4. 验证 'module_config' 的关键字段 + const auto &mod_cfg = stream["module_config"]; + for (const char *key : {"model_path", "label_path"}) { + if (!mod_cfg.contains(key) || !mod_cfg[key].is_string() || + mod_cfg[key].get().empty()) { + error_message = + "module_config missing or empty key: '" + std::string(key) + "'."; + return false; + } + } + if (!mod_cfg.contains("class_num") || + !mod_cfg["class_num"].is_number_integer() || + mod_cfg["class_num"].get() <= 0) { + error_message = "module_config missing or invalid 'class_num' (must be " + "integer > 0)."; + return false; + } + if (!mod_cfg.contains("rknn_thread_num") || + !mod_cfg["rknn_thread_num"].is_number_integer()) { + error_message = "module_config missing 'rknn_thread_num' (integer)."; + return false; + } + } + + return true; // 所有检查通过 + + } catch (const nlohmann::json::parse_error &e) { + error_message = "Invalid JSON: " + std::string(e.what()); + return false; + } catch (const std::exception &e) { + error_message = "Validation error: " + std::string(e.what()); + return false; + } +} + +bool WebServer::validate_main_config(const std::string &json_string, + std::string &error_message) { + try { + auto config = nlohmann::json::parse(json_string); + + if (!config.is_object()) { + error_message = "Root is not an object."; + return false; + } + + // 1. 必需的字符串类型 + for (const char *key : + {"device_id", "config_base_path", "mqtt_broker", + "mqtt_client_id_prefix", "data_storage_db_path", "data_cache_db_path", + "log_level", "alarm_rules_path", "piper_executable_path", + "piper_model_path", "video_config_path"}) { + if (!config.contains(key) || !config[key].is_string() || + config[key].get().empty()) { + error_message = + "Missing or invalid/empty string key: '" + std::string(key) + "'."; + return false; + } + } + + // 2. 必需的整数类型 + if (!config.contains("web_server_port") || + !config["web_server_port"].is_number_integer()) { + error_message = "Missing or invalid 'web_server_port' (must be integer)."; + return false; + } + + // 3. 必需的数组类型 + if (!config.contains("tcp_server_ports") || + !config["tcp_server_ports"].is_array()) { + error_message = "Missing or invalid 'tcp_server_ports' (must be array)."; + return false; + } + for (const auto &port : config["tcp_server_ports"]) { + if (!port.is_number_integer()) { + error_message = "Item in 'tcp_server_ports' is not an integer."; + return false; + } + } + + return true; // 所有检查通过 + + } catch (const nlohmann::json::parse_error &e) { + error_message = "Invalid JSON: " + std::string(e.what()); + return false; + } catch (const std::exception &e) { + error_message = "Validation error: " + std::string(e.what()); + return false; + } +} + +void WebServer::setup_routes() { + + CROW_ROUTE((*this), "/api/v1/system/id").methods("GET"_method)([this] { + auto deviceID = ConfigManager::getInstance().getDeviceID(); + crow::json::wvalue response; + response["deviceID"] = deviceID; + return response; + }); + + CROW_ROUTE((*this), "/api/v1/system/status").methods("GET"_method)([this] { + auto cpu_util = m_monitor.getCpuUtilization(); + auto mem_info = m_monitor.getMemoryInfo(); + + crow::json::wvalue response; + response["cpu_usage_percentage"] = cpu_util.totalUsagePercentage; + response["memory_total_kb"] = mem_info.total_kb; + response["memory_free_kb"] = mem_info.available_kb; + response["memory_usage_percentage"] = + (mem_info.total_kb > 0) + ? (1.0 - + static_cast(mem_info.available_kb) / mem_info.total_kb) * + 100.0 + : 0.0; + + return response; + }); + + CROW_ROUTE((*this), "/api/v1/devices").methods("GET"_method)([this] { + auto devices_info = m_device_manager.get_all_device_info(); + + std::vector devices_json; + for (const auto &info : devices_info) { + crow::json::wvalue device_obj; + + device_obj["id"] = info.id; + device_obj["type"] = info.type; + device_obj["is_running"] = info.is_running; + + crow::json::wvalue details_obj; + for (const auto &pair : info.connection_details) { + details_obj[pair.first] = pair.second; + } + device_obj["connection_details"] = std::move(details_obj); + + devices_json.push_back(std::move(device_obj)); + } + auto res = crow::response(crow::json::wvalue(devices_json)); + res.set_header("Content-Type", "application/json"); + return res; + }); + + CROW_ROUTE((*this), "/api/v1/data/latest").methods("GET"_method)([this] { + auto latest_data_map = m_live_data_cache.get_all_data(); + + crow::json::wvalue response; + for (const auto &pair : latest_data_map) { + response[pair.first] = crow::json::load(pair.second); + } + auto res = crow::response(response); + res.set_header("Content-Type", "application/json"); + return res; + }); + + CROW_ROUTE((*this), "/api/v1/alarms/active").methods("GET"_method)([this] { + try { + auto json_string = m_alarm_service.getActiveAlarmsJson().dump(); + + auto res = crow::response(200, json_string); + res.set_header("Content-Type", "application/json"); + return res; + + } catch (const std::exception &e) { + spdlog::error("Error processing /api/v1/alarms/active: {}", e.what()); + crow::json::wvalue error_resp; + error_resp["error"] = "Failed to retrieve active alarms."; + + auto res = crow::response(500, error_resp.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + CROW_ROUTE((*this), "/api/v1/alarms/history") + .methods("GET"_method)([this](const crow::request &req) { + int limit = 100; + if (req.url_params.get("limit")) { + try { + limit = std::stoi(req.url_params.get("limit")); + } catch (const std::exception &) { /* ignore invalid */ + } + } + if (limit <= 0) + limit = 100; + + try { + auto json_string = m_alarm_service.getAlarmHistoryJson(limit).dump(); + + auto res = crow::response(200, json_string); + res.set_header("Content-Type", "application/json"); + return res; + + } catch (const std::exception &e) { + spdlog::error("Error processing /api/v1/alarms/history: {}", e.what()); + crow::json::wvalue error_resp; + error_resp["error"] = "Failed to retrieve alarm history."; + + auto res = crow::response(500, error_resp.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + CROW_ROUTE((*this), "/api/v1/alarms/reload").methods("POST"_method)([this]() { + spdlog::info("Web API: Received request to reload alarm rules..."); + + bool success = m_alarm_service.reload_rules(); + + if (success) { + crow::json::wvalue response_json; + response_json["status"] = "success"; + response_json["message"] = "Alarm rules reloaded successfully."; + + auto res = crow::response(200, response_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + + } else { + crow::json::wvalue error_json; + error_json["status"] = "error"; + error_json["message"] = + "Failed to reload alarm rules. Check service logs for details."; + auto res = crow::response(500, error_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + CROW_ROUTE((*this), "/api/v1/alarms/clear") + .methods("POST"_method)([this](const crow::request &req) { + crow::json::rvalue j_body; + try { + j_body = crow::json::load(req.body); + } catch (const std::exception &e) { + spdlog::warn("Failed to parse request body for /api/v1/alarms/clear: {}", + e.what()); + auto res = crow::response(400, "{\"error\":\"Invalid JSON body.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + if (!j_body.has("rule_id") || !j_body.has("device_id")) { + auto res = crow::response( + 400, "{\"error\":\"Missing 'rule_id' or 'device_id'.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + std::string rule_id = j_body["rule_id"].s(); + std::string device_id = j_body["device_id"].s(); + + bool success = m_alarm_service.manually_clear_alarm(rule_id, device_id); + + if (success) { + auto res = crow::response( + 200, "{\"status\":\"success\", \"message\":\"Alarm cleared.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } else { + auto res = crow::response( + 404, "{\"status\":\"error\", \"message\":\"Alarm not " + "found or not active.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + CROW_ROUTE((*this), "/api/v1/alarms/config").methods("GET"_method)([this]() { + std::string rules_json_string = m_alarm_service.get_rules_as_json_string(); + auto res = crow::response(200, rules_json_string); + res.set_header("Content-Type", "application/json"); + return res; + }); + + CROW_ROUTE((*this), "/api/v1/alarms/config") + .methods("POST"_method)([this](const crow::request &req) { + const std::string &new_rules_content = req.body; + + bool success = + m_alarm_service.save_rules_from_json_string(new_rules_content); + + if (success) { + crow::json::wvalue response_json; + response_json["status"] = "success"; + response_json["message"] = + "Rules saved successfully. A reload is required to apply."; + + auto res = crow::response(200, response_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + + } else { + crow::json::wvalue error_json; + error_json["status"] = "error"; + error_json["message"] = + "Failed to save rules. Invalid JSON format or server error."; + + auto res = crow::response(400, error_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + /** + * @brief GET /api/v1/devices/config + * 获取设备配置 (devices.json) 的原始 JSON 字符串 + */ + CROW_ROUTE((*this), "/api/v1/devices/config").methods("GET"_method)([this]() { + std::string rules_json_string = + m_device_manager.get_config_as_json_string(); + auto res = crow::response(200, rules_json_string); + res.set_header("Content-Type", "application/json"); + return res; + }); + + /** + * @brief POST /api/v1/devices/config + * 保存设备配置 (devices.json) 的原始 JSON 字符串 + */ + CROW_ROUTE((*this), "/api/v1/devices/config") + .methods("POST"_method)([this](const crow::request &req) { + const std::string &new_rules_content = req.body; + + // save_config_from_json_string 内部包含 JSON 格式和 Schema 校验 + bool success = + m_device_manager.save_config_from_json_string(new_rules_content); + + if (success) { + crow::json::wvalue response_json; + response_json["status"] = "success"; + response_json["message"] = "Device config saved successfully. A " + "reload is required to apply."; + + auto res = crow::response(200, response_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + + } else { + crow::json::wvalue error_json; + error_json["status"] = "error"; + error_json["message"] = "Failed to save rules. Invalid JSON format " + "or schema. Check service logs."; + + auto res = crow::response(400, error_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + /** + * @brief POST /api/v1/devices/reload + * 通知后端从磁盘重载 devices.json 并应用 + */ + CROW_ROUTE((*this), "/api/v1/devices/reload").methods("POST"_method)([this]() { + spdlog::info("Web API: Received request to reload device rules..."); + + bool success = m_device_manager.reload_config_from_file(); + + if (success) { + crow::json::wvalue response_json; + response_json["status"] = "success"; + response_json["message"] = "Device rules reload posted successfully."; + + auto res = crow::response(200, response_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + + } else { + crow::json::wvalue error_json; + error_json["status"] = "error"; + error_json["message"] = + "Failed to post device rules reload. Check service logs."; + auto res = crow::response(500, error_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + }); + + /** + * @brief GET /api/v1/video_config + * 获取 video_config.json 的原始 JSON 字符串 + */ + CROW_ROUTE((*this), "/api/v1/video_config").methods("GET"_method)([this]() { + std::string config_path; + try { + + config_path = ConfigManager::getInstance().getVideoConfigPath(); + } catch (const std::exception &e) { + spdlog::error("Failed to get video config path from ConfigManager: {}", + e.what()); + auto res = crow::response(500, "{\"error\":\"Server configuration error: " + "cannot determine config path.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + std::ifstream ifs(config_path); + if (!ifs.is_open()) { + spdlog::error("Failed to open video config file for reading: {}", + config_path); + auto res = + crow::response(404, "{\"error\":\"Video config file not found.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + std::string content((std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator())); + + auto res = crow::response(200, content); + res.set_header("Content-Type", "application/json"); + return res; + }); + + /** + * @brief POST /api/v1/video_config + * 验证并保存 video_config.json + */ + CROW_ROUTE((*this), "/api/v1/video_config") + .methods("POST"_method)([this](const crow::request &req) { + const std::string &new_config_content = req.body; + std::string error_msg; + + if (!validate_video_config(new_config_content, error_msg)) { + spdlog::warn("Web API: Failed to save video_config: {}", error_msg); + crow::json::wvalue error_json; + error_json["status"] = "error"; + error_json["message"] = "Invalid JSON format or schema: " + error_msg; + auto res = crow::response(400, error_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + + std::string config_path = + ConfigManager::getInstance().getVideoConfigPath(); + + std::ofstream ofs(config_path); + if (!ofs.is_open()) { + spdlog::error("Failed to open video config file for writing: {}", + config_path); + auto res = crow::response( + 500, "{\"error\":\"Failed to write config file on server.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + auto json_data = nlohmann::json::parse(new_config_content); + ofs << json_data.dump(4); // 格式化写入 + ofs.close(); + + // 4. 成功响应 + spdlog::info("Video config successfully updated via API at {}", + config_path); + crow::json::wvalue response_json; + response_json["status"] = "success"; + response_json["message"] = + "Video config saved. Send POST to /api/v1/service/reload to apply."; + auto res = crow::response(200, response_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + }); + + CROW_ROUTE((*this), "/api/v1/service/reload").methods("POST"_method)([this]() { + if (m_shutdown_handler) { + spdlog::info("Web API: Received request to reload service..."); + m_shutdown_handler(); + + auto res = + crow::response(202, "{\"status\":\"restarting\", " + "\"message\":\"Service is restarting...\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + spdlog::error("Web API: /api/v1/service/reload called, but shutdown handler " + "is not set!"); + auto res = crow::response( + 500, "{\"error\":\"Shutdown handler not configured on server.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + }); + + + /** + * @brief GET /api/v1/config + * 获取主 config.json 的原始 JSON 字符串 + */ + CROW_ROUTE((*this), "/api/v1/config").methods("GET"_method)([this]() { + std::string config_path; + try { + // 1. 从 ConfigManager 获取路径 + config_path = ConfigManager::getInstance().getConfigFilePath(); + if (config_path.empty()) { + throw std::runtime_error("ConfigManager returned an empty path."); + } + } catch (const std::exception &e) { + spdlog::error("Failed to get main config path from ConfigManager: {}", + e.what()); + auto res = crow::response(500, "{\"error\":\"Server configuration error: " + "cannot determine config path.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + // 2. 读取文件 + std::ifstream ifs(config_path); + if (!ifs.is_open()) { + spdlog::error("Failed to open main config file for reading: {}", + config_path); + auto res = + crow::response(404, "{\"error\":\"Main config file not found.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + // 3. 返回内容 + std::string content((std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator())); + + auto res = crow::response(200, content); + res.set_header("Content-Type", "application/json"); + return res; + }); + + /** + * @brief POST /api/v1/config + * 验证并保存主 config.json + */ + CROW_ROUTE((*this), "/api/v1/config") + .methods("POST"_method)([this](const crow::request &req) { + const std::string &new_config_content = req.body; + std::string error_msg; + + if (!validate_main_config(new_config_content, error_msg)) { + spdlog::warn("Web API: Failed to save main config: {}", error_msg); + crow::json::wvalue error_json; + error_json["status"] = "error"; + error_json["message"] = "Invalid JSON format or schema: " + error_msg; + auto res = crow::response(400, error_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + } + + std::string config_path = + ConfigManager::getInstance().getConfigFilePath(); + if (config_path.empty()) { + auto res = crow::response( + 500, "{\"error\":\"Failed to get config file path on server.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + std::ofstream ofs(config_path); + if (!ofs.is_open()) { + spdlog::error("Failed to open main config file for writing: {}", + config_path); + auto res = crow::response( + 500, "{\"error\":\"Failed to write config file on server.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + try { + auto json_data = nlohmann::json::parse(new_config_content); + ofs << json_data.dump(4); + ofs.close(); + } catch (const std::exception &e) { + spdlog::error("Failed to re-parse and dump main config: {}", + e.what()); + auto res = crow::response( + 500, "{\"error\":\"Failed to serialize config for writing.\"}"); + res.set_header("Content-Type", "application/json"); + return res; + } + + spdlog::info("Main config successfully updated via API at {}", + config_path); + crow::json::wvalue response_json; + response_json["status"] = "success"; + response_json["message"] = + "Main config saved. A full service restart (e.g., via " + "/api/v1/service/reload) is required to apply changes."; + auto res = crow::response(200, response_json.dump()); + res.set_header("Content-Type", "application/json"); + return res; + }); +} \ No newline at end of file diff --git a/src/web/web_server.h b/src/web/web_server.h index fe4baf6..e91f5f0 100644 --- a/src/web/web_server.h +++ b/src/web/web_server.h @@ -1,50 +1,47 @@ #pragma once -#include "crow.h" -#include "crow/middlewares/cors.h" - -#include "alarm/alarm_service.h" -#include "dataCache/live_data_cache.h" -#include "deviceManager/device_manager.h" -#include "systemMonitor/system_monitor.h" - -#include "nlohmann/json.hpp" #include #include +#include "alarm/alarm_service.h" +#include "crow.h" +#include "crow/middlewares/cors.h" +#include "dataCache/live_data_cache.h" +#include "deviceManager/device_manager.h" +#include "nlohmann/json.hpp" +#include "systemMonitor/system_monitor.h" + class WebServer : public crow::Crow { - public: - WebServer(SystemMonitor::SystemMonitor &monitor, DeviceManager &deviceManager, - LiveDataCache &liveDataCache, AlarmService &alarm_service, - uint16_t port = 8080); - ~WebServer(); + WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager, + LiveDataCache& liveDataCache, AlarmService& alarm_service, uint16_t port = 8080); + ~WebServer(); - WebServer(const WebServer &) = delete; - WebServer &operator=(const WebServer &) = delete; + WebServer(const WebServer&) = delete; + WebServer& operator=(const WebServer&) = delete; - void start(); - void stop(); + void start(); + void stop(); - void set_shutdown_handler(std::function handler); + void set_shutdown_handler(std::function handler); private: - void setup_routes(); + void setup_routes(); - bool validate_video_config(const std::string &json_string, - std::string &error_message); + // bool validate_video_config(const std::string &json_string, + // std::string &error_message); - bool validate_main_config(const std::string &json_string, - std::string &error_message); + // bool validate_main_config(const std::string &json_string, + // std::string &error_message); - SystemMonitor::SystemMonitor &m_monitor; - DeviceManager &m_device_manager; - LiveDataCache &m_live_data_cache; - AlarmService &m_alarm_service; + SystemMonitor::SystemMonitor& m_monitor; + DeviceManager& m_device_manager; + LiveDataCache& m_live_data_cache; + AlarmService& m_alarm_service; - uint16_t m_port; + uint16_t m_port; - std::thread m_thread; + std::thread m_thread; - std::function m_shutdown_handler; + std::function m_shutdown_handler; };