diff --git a/src/main.cpp b/src/main.cpp index ccef16f..3037cce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -86,7 +86,6 @@ void poll_system_metrics(boost::asio::steady_timer &timer, } int main(int argc, char *argv[]) { - // ... (ConfigManager 加载保持不变) ... const std::string config_path = "/app/config/config.json"; if (!ConfigManager::getInstance().load(config_path)) { std::cerr << "Failed to load configuration from " << config_path @@ -96,7 +95,6 @@ int main(int argc, char *argv[]) { auto &config = ConfigManager::getInstance(); - // ... (spdlog, DataStorage 初始化保持不变) ... try { spdlog::set_level(spdlog::level::from_str(config.getLogLevel())); spdlog::info("Edge Proxy starting up..."); @@ -191,6 +189,10 @@ int main(int argc, char *argv[]) { WebServer web_server(monitor, device_manager, live_data_cache, alarm_service, config.getWebServerPort()); + web_server.set_shutdown_handler([&]() { + spdlog::warn("Received shutdown command from Web API. Shutting down."); + g_io_context.stop(); // <-- 这将触发 signals.async_wait 下方的所有清理逻辑 + }); web_server.start(); // ----------------------------------------------------------------- @@ -240,7 +242,7 @@ int main(int argc, char *argv[]) { video_manager.stop_all(); spdlog::info("[Shutdown] F. De-initializing postprocess library..."); - deinitPostProcess(); + deinitPostProcess(); // f. 最后,安全地停止io_context spdlog::info("[Shutdown] G. Stopping main event loop..."); diff --git a/src/rknn/video_service.cc b/src/rknn/video_service.cc index da0489b..9641b49 100644 --- a/src/rknn/video_service.cc +++ b/src/rknn/video_service.cc @@ -93,8 +93,8 @@ bool VideoService::start() { "video/x-raw,format=BGR ! " "videoconvert ! " "video/x-raw,format=NV12 ! " - "mpph265enc gop=25 rc-mode=fixqp qp-init=26 ! " - "h265parse ! " + "mpph264enc gop=25 rc-mode=fixqp qp-init=26 ! " + "h264parse ! " "rtspclientsink location=" + output_rtsp_url_ + " latency=0 protocols=tcp"; diff --git a/src/web/web_server.cc b/src/web/web_server.cc index 8c18bd7..63f18db 100644 --- a/src/web/web_server.cc +++ b/src/web/web_server.cc @@ -1,10 +1,10 @@ -// 文件名: src/web/web_server.cc #include "web_server.h" #include #include #include "config/config_manager.h" +#include "nlohmann/json.hpp" #include "spdlog/spdlog.h" WebServer::WebServer(SystemMonitor::SystemMonitor &monitor, @@ -44,7 +44,106 @@ void WebServer::stop() { } } +// [新增] set_shutdown_handler 的实现 +void WebServer::set_shutdown_handler(std::function handler) { + m_shutdown_handler = handler; +} + +// [新增] validate_video_config 的实现 +bool WebServer::validate_video_config(const std::string &json_string, + std::string &error_message) { + try { + // 我们使用 nlohmann::json 来解析和验证 + 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; + } +} + void WebServer::setup_routes() { + + // --- 原始路由 (未省略) --- + CROW_ROUTE((*this), "/api/system/id").methods("GET"_method)([this] { auto deviceID = ConfigManager::getInstance().getDeviceID(); crow::json::wvalue response; @@ -101,11 +200,8 @@ void WebServer::setup_routes() { for (const auto &pair : latest_data_map) { response[pair.first] = crow::json::load(pair.second); } - // 1. 将 JSON 对象转换为 crow::response auto res = crow::response(response); - // 2. 明确设置 Content-Type 头部 res.set_header("Content-Type", "application/json"); - // 3. 返回 response 对象 return res; }); @@ -258,7 +354,7 @@ void WebServer::setup_routes() { } }); - // --- [!! 新增路由: 设备配置管理 !!] --- + // --- [!! 原始路由: 设备配置管理 !!] --- /** * @brief GET /api/devices/config @@ -334,4 +430,119 @@ void WebServer::setup_routes() { 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 { + // 从 ConfigManager 获取 video_config.json 的路径 + 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; + + // 1. [!! 验证 !!] + 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; + } + + // 2. 获取路径 + std::string config_path = + ConfigManager::getInstance().getVideoConfigPath(); + + // 3. 写入文件 + 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; + } + + // 我们将 nlohmann::json 解析后的内容重新 dump 写入 + // 这样可以确保写入的 JSON 格式是规范的 (例如,格式化) + 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; + }); } \ No newline at end of file diff --git a/src/web/web_server.h b/src/web/web_server.h index 6eedbb4..87e666c 100644 --- a/src/web/web_server.h +++ b/src/web/web_server.h @@ -1,47 +1,47 @@ -// 文件名: src/web/web_server.h -#ifndef WEB_SERVER_H -#define WEB_SERVER_H +#pragma once #include "crow.h" -#include "crow/middlewares/cors.h" +#include "crow/middlewares/cors.h" -#include "systemMonitor/system_monitor.h" -#include "deviceManager/device_manager.h" -#include "dataCache/live_data_cache.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 - 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); private: - void setup_routes(); + void setup_routes(); - SystemMonitor::SystemMonitor& m_monitor; - DeviceManager& m_device_manager; - LiveDataCache& m_live_data_cache; - AlarmService& m_alarm_service; - + bool validate_video_config(const std::string &json_string, + std::string &error_message); - uint16_t m_port; - - std::thread m_thread; + SystemMonitor::SystemMonitor &m_monitor; + DeviceManager &m_device_manager; + LiveDataCache &m_live_data_cache; + AlarmService &m_alarm_service; + + uint16_t m_port; + + std::thread m_thread; + + std::function m_shutdown_handler; }; - -#endif // WEB_SERVER_H \ No newline at end of file