2025-10-15 14:27:21 +08:00
|
|
|
|
#include "web_server.h"
|
2025-11-03 09:37:54 +08:00
|
|
|
|
|
|
|
|
|
|
#include <fstream>
|
|
|
|
|
|
#include <sstream>
|
|
|
|
|
|
|
|
|
|
|
|
#include "config/config_manager.h"
|
2025-11-04 19:46:01 +08:00
|
|
|
|
#include "nlohmann/json.hpp"
|
2025-10-15 14:27:21 +08:00
|
|
|
|
#include "spdlog/spdlog.h"
|
|
|
|
|
|
|
2026-01-21 11:04:34 +08:00
|
|
|
|
WebServer::WebServer(SystemMonitor::SystemMonitor& monitor, DeviceManager& deviceManager,
|
|
|
|
|
|
LiveDataCache& liveDataCache, AlarmService& alarm_service,
|
2026-01-22 16:29:57 +08:00
|
|
|
|
LightController& lightController, DoorController& doorController,
|
|
|
|
|
|
VideoServiceManager& videoManager, // [修复 5.2] 对应头文件添加参数
|
2026-01-21 11:04:34 +08:00
|
|
|
|
uint16_t port)
|
|
|
|
|
|
: crow::Crow<crow::CORSHandler>(),
|
|
|
|
|
|
m_monitor(monitor),
|
|
|
|
|
|
m_device_manager(deviceManager),
|
|
|
|
|
|
m_live_data_cache(liveDataCache),
|
|
|
|
|
|
m_alarm_service(alarm_service),
|
|
|
|
|
|
m_light_controller(lightController), // <--- 初始化成员
|
|
|
|
|
|
m_door_controller(doorController),
|
2026-01-22 16:29:57 +08:00
|
|
|
|
m_video_manager(videoManager),
|
2026-01-21 11:04:34 +08:00
|
|
|
|
m_port(port) {
|
|
|
|
|
|
auto& cors = this->get_middleware<crow::CORSHandler>();
|
|
|
|
|
|
cors.global()
|
|
|
|
|
|
.origin("*")
|
|
|
|
|
|
.headers("Content-Type", "Authorization")
|
|
|
|
|
|
.methods("GET"_method, "POST"_method, "OPTIONS"_method);
|
|
|
|
|
|
|
|
|
|
|
|
this->loglevel(crow::LogLevel::Warning);
|
|
|
|
|
|
setup_routes();
|
2025-10-15 14:27:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 11:04:34 +08:00
|
|
|
|
WebServer::~WebServer() {
|
|
|
|
|
|
stop();
|
|
|
|
|
|
}
|
2025-11-03 09:37:54 +08:00
|
|
|
|
|
2025-10-15 14:27:21 +08:00
|
|
|
|
void WebServer::start() {
|
2026-01-21 11:04:34 +08:00
|
|
|
|
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.");
|
|
|
|
|
|
});
|
2025-10-15 14:27:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WebServer::stop() {
|
2026-01-21 11:04:34 +08:00
|
|
|
|
crow::Crow<crow::CORSHandler>::stop();
|
|
|
|
|
|
if (m_thread.joinable()) {
|
|
|
|
|
|
m_thread.join();
|
|
|
|
|
|
}
|
2025-10-15 14:27:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 19:46:01 +08:00
|
|
|
|
void WebServer::set_shutdown_handler(std::function<void()> handler) {
|
2026-01-21 11:04:34 +08:00
|
|
|
|
m_shutdown_handler = handler;
|
2025-11-04 19:46:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 11:04:34 +08:00
|
|
|
|
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<std::string>().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<std::string>().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<int>() <= 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;
|
|
|
|
|
|
}
|
2025-11-04 19:46:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 11:04:34 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-11-05 14:50:25 +08:00
|
|
|
|
}
|
2025-11-04 19:46:01 +08:00
|
|
|
|
|
2025-11-05 14:50:25 +08:00
|
|
|
|
void WebServer::setup_routes() {
|
2026-01-21 11:04:34 +08:00
|
|
|
|
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<double>(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<crow::json::wvalue> 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<char>(ifs)),
|
|
|
|
|
|
(std::istreambuf_iterator<char>()));
|
|
|
|
|
|
|
|
|
|
|
|
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<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;
|
|
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 灯光控制接口
|
|
|
|
|
|
* @method POST
|
|
|
|
|
|
* @url /api/light/control
|
|
|
|
|
|
* @body JSON { "device_id": "351", "action": "0" }
|
|
|
|
|
|
* - device_id: 灯控设备的ID
|
|
|
|
|
|
* - action: "0" (关) 或 "1" (开)
|
|
|
|
|
|
*/
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/light/control")
|
|
|
|
|
|
.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("Invalid JSON in light control request: {}", e.what());
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Invalid JSON format.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 参数校验
|
|
|
|
|
|
if (!j_body.has("device_id") || !j_body.has("action")) {
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Missing 'device_id' or 'action'.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string deviceId = j_body["device_id"].s();
|
|
|
|
|
|
std::string action = j_body["action"].s(); // "0" or "1"
|
|
|
|
|
|
|
|
|
|
|
|
spdlog::info("Web API: Requesting light control. Device: {}, Action: {}", deviceId,
|
|
|
|
|
|
action);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用控制器
|
|
|
|
|
|
bool success = m_light_controller.ControlLight(deviceId, action);
|
|
|
|
|
|
|
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
|
response_json["message"] = "Light control command sent.";
|
|
|
|
|
|
return crow::response(200, response_json);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
response_json["status"] = "error";
|
|
|
|
|
|
response_json["message"] = "Failed to send control command (upstream error).";
|
|
|
|
|
|
return crow::response(502, response_json);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-22 16:29:57 +08:00
|
|
|
|
/**
|
2026-01-21 11:04:34 +08:00
|
|
|
|
* @brief 门禁控制接口
|
|
|
|
|
|
* @method POST
|
|
|
|
|
|
* @url /api/door/control
|
|
|
|
|
|
* @body JSON { "action": "open" }
|
|
|
|
|
|
* - action: "open" (开门), "close" (关门), "alwaysOpen" (常开), "alwaysClose" (常闭)
|
|
|
|
|
|
*/
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/door/control")
|
|
|
|
|
|
.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("Invalid JSON in door control request: {}", e.what());
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Invalid JSON format.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 参数校验
|
|
|
|
|
|
if (!j_body.has("action")) {
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Missing 'action' field.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string action = j_body["action"].s();
|
2026-01-22 16:29:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 简单的安全检查,防止注入
|
|
|
|
|
|
if (action != "open" && action != "close" && action != "alwaysOpen" &&
|
|
|
|
|
|
action != "alwaysClose") {
|
|
|
|
|
|
return crow::response(400,
|
|
|
|
|
|
"{\"error\":\"Invalid action. Use 'open', 'close', etc.\"}");
|
|
|
|
|
|
}
|
2026-01-21 11:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
spdlog::info("Web API: Requesting door control. Action: {}", action);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用控制器
|
|
|
|
|
|
bool success = m_door_controller.ControlDoor(action);
|
|
|
|
|
|
|
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
|
response_json["message"] = "Door control command sent.";
|
|
|
|
|
|
return crow::response(200, response_json);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
response_json["status"] = "error";
|
|
|
|
|
|
response_json["message"] = "Failed to send control command to Hikvision device.";
|
|
|
|
|
|
return crow::response(502, response_json);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-22 16:29:57 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief POST /api/video/zone
|
|
|
|
|
|
* 单独修改指定视频流的 intrusion_zone (多边形区域)
|
|
|
|
|
|
* @body JSON:
|
|
|
|
|
|
* {
|
|
|
|
|
|
* "stream_id": "cam_01_intrusion",
|
|
|
|
|
|
* "intrusion_zone": [[100, 100], [500, 100], [500, 500], [100, 500]]
|
|
|
|
|
|
* }
|
|
|
|
|
|
*/
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/video/zone").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("Invalid JSON in update zone request: {}", e.what());
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Invalid JSON format.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 参数校验
|
|
|
|
|
|
if (!j_body.has("stream_id") || !j_body.has("intrusion_zone")) {
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Missing 'stream_id' or 'intrusion_zone'.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string target_stream_id = j_body["stream_id"].s();
|
|
|
|
|
|
const auto& new_zone = j_body["intrusion_zone"];
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 intrusion_zone 是否为数组,且至少有3个点(构成多边形)
|
|
|
|
|
|
if (new_zone.t() != crow::json::type::List || new_zone.size() < 3) {
|
|
|
|
|
|
return crow::response(
|
|
|
|
|
|
400, "{\"error\":\"'intrusion_zone' must be an array with at least 3 points.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 读取当前配置文件
|
|
|
|
|
|
std::string config_path = ConfigManager::getInstance().getVideoConfigPath();
|
|
|
|
|
|
std::ifstream ifs(config_path);
|
|
|
|
|
|
if (!ifs.is_open()) {
|
|
|
|
|
|
spdlog::error("Failed to open video config file: {}", config_path);
|
|
|
|
|
|
return crow::response(500, "{\"error\":\"Config file not found.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json json_config;
|
|
|
|
|
|
try {
|
|
|
|
|
|
ifs >> json_config;
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
|
ifs.close();
|
|
|
|
|
|
spdlog::error("Failed to parse video config file: {}", e.what());
|
|
|
|
|
|
return crow::response(500, "{\"error\":\"Existing config file is corrupt.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
ifs.close();
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 查找并更新目标流
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
if (json_config.contains("video_streams") && json_config["video_streams"].is_array()) {
|
|
|
|
|
|
for (auto& stream : json_config["video_streams"]) {
|
|
|
|
|
|
if (stream.contains("id") && stream["id"] == target_stream_id) {
|
|
|
|
|
|
// 确保 module_config 存在
|
|
|
|
|
|
if (!stream.contains("module_config")) {
|
|
|
|
|
|
stream["module_config"] = nlohmann::json::object();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换 crow::json 到 nlohmann::json
|
|
|
|
|
|
// 注意:这里需要手动转换一下格式,或者直接解析 req.body 为 nlohmann::json
|
|
|
|
|
|
try {
|
|
|
|
|
|
auto body_nlohmann = nlohmann::json::parse(req.body);
|
|
|
|
|
|
stream["module_config"]["intrusion_zone"] = body_nlohmann["intrusion_zone"];
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
|
return crow::response(
|
|
|
|
|
|
400, "{\"error\":\"Failed to parse intrusion_zone data.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
|
|
return crow::response(404, "{\"error\":\"Stream ID not found.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 写回文件
|
|
|
|
|
|
std::ofstream ofs(config_path);
|
|
|
|
|
|
if (!ofs.is_open()) {
|
|
|
|
|
|
spdlog::error("Failed to open video config file for writing: {}", config_path);
|
|
|
|
|
|
return crow::response(500, "{\"error\":\"Failed to save config file.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ofs << json_config.dump(4); // 4空格缩进
|
|
|
|
|
|
ofs.close();
|
|
|
|
|
|
|
|
|
|
|
|
spdlog::info("Updated intrusion_zone for stream: {}", target_stream_id);
|
|
|
|
|
|
|
|
|
|
|
|
crow::json::wvalue response_json;
|
|
|
|
|
|
response_json["status"] = "success";
|
|
|
|
|
|
response_json["message"] = "Zone updated. Call /api/service/reload to apply changes.";
|
|
|
|
|
|
|
|
|
|
|
|
return crow::response(200, response_json);
|
|
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 切换检测模式接口
|
|
|
|
|
|
* @method POST
|
|
|
|
|
|
* @url /api/video/mode
|
|
|
|
|
|
* @body JSON { "stream_id": "cam_01", "mode": 1 }
|
|
|
|
|
|
* - mode: 1 (普通报警), 2 (快速响应)
|
|
|
|
|
|
*/
|
|
|
|
|
|
CROW_ROUTE((*this), "/api/video/mode").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) {
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Invalid JSON.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!j_body.has("stream_id") || !j_body.has("mode")) {
|
|
|
|
|
|
return crow::response(400, "{\"error\":\"Missing 'stream_id' or 'mode'.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string stream_id = j_body["stream_id"].s();
|
|
|
|
|
|
int mode = j_body["mode"].i();
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 【运行时更新】 立即生效
|
|
|
|
|
|
auto service = m_video_manager.getService(stream_id);
|
|
|
|
|
|
if (service) {
|
|
|
|
|
|
// 调用内存中的对象修改状态
|
|
|
|
|
|
bool result = service->set_analysis_mode(mode);
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
spdlog::warn("Failed to set runtime mode for stream {}", stream_id);
|
|
|
|
|
|
// 注意:这里即使运行时失败,只要配置能保存,也可以继续往下走,或者直接返回错误
|
|
|
|
|
|
// return crow::response(400, "{\"status\":\"error\", \"message\":\"Failed to switch
|
|
|
|
|
|
// runtime mode.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果服务没运行(比如禁用了),我们依然允许修改配置文件,这样下次启动就生效了
|
|
|
|
|
|
spdlog::warn("Stream {} not running, but config will be updated.", stream_id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 【持久化更新】 保存到 JSON 文件
|
|
|
|
|
|
std::string config_path = ConfigManager::getInstance().getVideoConfigPath();
|
|
|
|
|
|
std::ifstream ifs(config_path);
|
|
|
|
|
|
if (!ifs.is_open()) {
|
|
|
|
|
|
return crow::response(500, "{\"error\":\"Config file not found.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
nlohmann::json json_config;
|
|
|
|
|
|
try {
|
|
|
|
|
|
ifs >> json_config;
|
|
|
|
|
|
} catch (...) {
|
|
|
|
|
|
return crow::response(500, "{\"error\":\"Config file corrupt.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
ifs.close();
|
|
|
|
|
|
|
|
|
|
|
|
bool found_config = false;
|
|
|
|
|
|
if (json_config.contains("video_streams") && json_config["video_streams"].is_array()) {
|
|
|
|
|
|
for (auto& stream : json_config["video_streams"]) {
|
|
|
|
|
|
if (stream.contains("id") && stream["id"] == stream_id) {
|
|
|
|
|
|
// 找到对应流,修改 module_config 中的 mode
|
|
|
|
|
|
if (!stream.contains("module_config")) {
|
|
|
|
|
|
stream["module_config"] = nlohmann::json::object();
|
|
|
|
|
|
}
|
|
|
|
|
|
stream["module_config"]["mode"] = mode; // [关键] 写入 mode
|
|
|
|
|
|
found_config = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!found_config) {
|
|
|
|
|
|
return crow::response(404, "{\"error\":\"Stream ID not found in config.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写回文件
|
|
|
|
|
|
std::ofstream ofs(config_path);
|
|
|
|
|
|
if (!ofs.is_open()) {
|
|
|
|
|
|
return crow::response(500, "{\"error\":\"Failed to save config.\"}");
|
|
|
|
|
|
}
|
|
|
|
|
|
ofs << json_config.dump(4);
|
|
|
|
|
|
ofs.close();
|
|
|
|
|
|
|
|
|
|
|
|
spdlog::info("Updated mode to {} for stream {} (Saved to file)", mode, stream_id);
|
|
|
|
|
|
|
|
|
|
|
|
return crow::response(200,
|
|
|
|
|
|
"{\"status\":\"success\", \"message\":\"Mode switched and saved.\"}");
|
|
|
|
|
|
});
|
2025-10-15 14:27:21 +08:00
|
|
|
|
}
|