增加配置管理端点

This commit is contained in:
GuanYuankai 2025-11-05 06:50:25 +00:00
parent b14e2cba8e
commit 0b4fa12325
5 changed files with 187 additions and 90 deletions

View File

@ -7,41 +7,41 @@
"enabled": true,
"id": "cam_01_intrusion",
"input_url": "rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1301",
"module_type": "intrusion_detection",
"output_rtsp": "rtsp://127.0.0.1:8554/ch1301",
"module_config": {
"model_path": "/app/edge-proxy/models/RK3588/yolov5s-640-640.rknn",
"rknn_thread_num": 3,
"class_num": 80,
"intrusion_zone": [
100,
100,
300,
300
1820,
1820
],
"time_threshold_sec": 3.0,
"class_num": 80,
"label_path": "/app/edge-proxy/models/coco_80_labels_list.txt"
}
"label_path": "/app/edge-proxy/models/coco_80_labels_list.txt",
"model_path": "/app/edge-proxy/models/RK3588/yolov5s-640-640.rknn",
"rknn_thread_num": 3,
"time_threshold_sec": 3
},
"module_type": "intrusion_detection",
"output_rtsp": "rtsp://127.0.0.1:8554/ch1301"
},
{
"enabled": true,
"id": "cam_03_intrusion",
"input_url": "rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1501",
"module_type": "intrusion_detection",
"output_rtsp": "rtsp://127.0.0.1:8554/ch1501",
"module_config": {
"model_path": "/app/edge-proxy/models/RK3588/yolov5s-640-640.rknn",
"rknn_thread_num": 3,
"class_num": 80,
"intrusion_zone": [
100,
100,
300,
300
],
"time_threshold_sec": 3.0,
"class_num": 80,
"label_path": "/app/edge-proxy/models/coco_80_labels_list.txt"
}
"label_path": "/app/edge-proxy/models/coco_80_labels_list.txt",
"model_path": "/app/edge-proxy/models/RK3588/yolov5s-640-640.rknn",
"rknn_thread_num": 3,
"time_threshold_sec": 3
},
"module_type": "intrusion_detection",
"output_rtsp": "rtsp://127.0.0.1:8554/ch1501"
}
]
}

View File

@ -140,56 +140,7 @@ std::string ConfigManager::getVideoConfigPath() {
get<std::string>("video_config_path", "video_config.json");
}
// bool ConfigManager::getIsVideoServiceEnabled() const {
// std::shared_lock<std::shared_mutex> lock(m_mutex);
// try {
// if (m_config_json.contains("video_service")) {
// return m_config_json["video_service"].value("enabled", false);
// }
// } catch (const json::type_error &e) {
// spdlog::warn(
// "Config type mismatch for key 'video_service.enabled'. Error: {}",
// e.what());
// }
// return false;
// }
// std::vector<ConfigManager::VideoStreamConfig>
// ConfigManager::getVideoStreamConfigs() const {
// std::vector<VideoStreamConfig> configs;
// std::shared_lock<std::shared_mutex> lock(m_mutex);
// try {
// if (m_config_json.contains("video_streams") &&
// m_config_json["video_streams"].is_array()) {
// for (const auto &stream_json : m_config_json["video_streams"]) {
// VideoStreamConfig cfg;
// cfg.id = stream_json.value("id", "");
// cfg.enabled = stream_json.value("enabled", false);
// cfg.input_url = stream_json.value("input_url", "");
// cfg.output_rtsp = stream_json.value("output_rtsp", "");
// cfg.module_type = stream_json.value("module_type", "");
// cfg.module_config = stream_json.value("module_config",
// json::object());
// if (cfg.module_type.empty()) {
// spdlog::warn("Video stream '{}' has no 'module_type' defined. It
// may "
// "fail to start.",
// cfg.id);
// }
// configs.push_back(cfg);
// }
// } else {
// spdlog::warn("Config key 'video_streams' not found or is not an
// array.");
// }
// } catch (const json::exception &e) {
// spdlog::error("Error parsing 'video_streams': {}", e.what());
// }
// return configs;
// }
std::string ConfigManager::getConfigFilePath() const {
std::shared_lock<std::shared_mutex> lock(m_mutex);
return m_configFilePath;
}

View File

@ -76,6 +76,7 @@ public:
// bool getIsVideoServiceEnabled() const;
// std::vector<VideoStreamConfig> getVideoStreamConfigs() const;
std::string getVideoConfigPath();
std::string getConfigFilePath() const;
private:
ConfigManager() = default;

View File

@ -44,16 +44,13 @@ void WebServer::stop() {
}
}
// [新增] set_shutdown_handler 的实现
void WebServer::set_shutdown_handler(std::function<void()> 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()) {
@ -140,9 +137,62 @@ bool WebServer::validate_video_config(const std::string &json_string,
}
}
void WebServer::setup_routes() {
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<std::string>().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();
@ -354,8 +404,6 @@ void WebServer::setup_routes() {
}
});
// --- [!! 原始路由: 设备配置管理 !!] ---
/**
* @brief GET /api/devices/config
* (devices.json) JSON
@ -431,10 +479,6 @@ void WebServer::setup_routes() {
}
});
// -----------------------------------------------------------------
// [!! 新增: 视频配置路由 (完整版) !!]
// -----------------------------------------------------------------
/**
* @brief GET /api/video_config
* video_config.json JSON
@ -442,7 +486,7 @@ void WebServer::setup_routes() {
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: {}",
@ -463,7 +507,6 @@ void WebServer::setup_routes() {
return res;
}
// 读取文件全部内容
std::string content((std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>()));
@ -481,7 +524,6 @@ void WebServer::setup_routes() {
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;
@ -492,11 +534,9 @@ void WebServer::setup_routes() {
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: {}",
@ -507,8 +547,6 @@ void WebServer::setup_routes() {
return res;
}
// 我们将 nlohmann::json 解析后的内容重新 dump 写入
// 这样可以确保写入的 JSON 格式是规范的 (例如,格式化)
auto json_data = nlohmann::json::parse(new_config_content);
ofs << json_data.dump(4); // 格式化写入
ofs.close();
@ -525,7 +563,6 @@ void WebServer::setup_routes() {
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...");
@ -545,4 +582,109 @@ void WebServer::setup_routes() {
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<char>(ifs)),
(std::istreambuf_iterator<char>()));
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;
});
}

View File

@ -34,6 +34,9 @@ private:
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);
SystemMonitor::SystemMonitor &m_monitor;
DeviceManager &m_device_manager;
LiveDataCache &m_live_data_cache;