bonus-edge-proxy/src/web/web_server.cc

1041 lines
34 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "web_server.h"
#include <fstream>
#include <sstream>
#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,
LightController& lightController, DoorController& doorController,
VideoServiceManager& videoManager, // [修复 5.2] 对应头文件添加参数
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),
m_video_manager(videoManager),
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();
}
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<crow::CORSHandler>::stop();
if (m_thread.joinable()) {
m_thread.join();
}
}
void WebServer::set_shutdown_handler(std::function<void()> 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<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;
}
}
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();
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) {
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 req_deviceId = j_body["device_id"].s();
std::string action = j_body["action"].s();
spdlog::info("Web API: Light Control. Device: {}, Action: {}", req_deviceId, action);
// 1. 定义要控制的设备列表 (将前端的联动逻辑移到这里)
std::vector<std::string> target_devices;
// 如果是总控开关 351则添加所有相关设备
if (req_deviceId == "351") {
// 顺序很重要,建议把不太重要的放后面
target_devices = {"353", "350", "351", "352", "354", "349"};
} else {
// 普通设备,只控制自己
target_devices.push_back(req_deviceId);
}
// 2. [关键优化] 启动后台线程执行物理控制 (Fire-and-Forget)
// 这样前端会立刻收到 200 OK切换页面也不会断连
// 复制必要的变量到 lambda
LightController* ctrl = &m_light_controller;
std::string act = action;
std::vector<std::string> thread_targets = target_devices; // 拷贝一份
std::thread([ctrl, thread_targets, act]() {
for (const auto& id : thread_targets) {
// 依次控制,防止网关拥堵
bool ok = ctrl->ControlLight(id, act);
if (!ok) {
// 失败稍微停顿一下
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// 成功也稍微停顿一下,让网关喘口气 (例如 20ms)
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
spdlog::info("Async Light Control Finished for {} devices.", thread_targets.size());
}).detach(); // 分离线程
// 3. 处理"智能模式"冲突 (保持之前的逻辑,但要针对列表里的所有设备)
// 也就是:如果我手动关了灯,那么关联这个灯的摄像头必须退出智能模式
try {
std::string config_path = ConfigManager::getInstance().getVideoConfigPath();
std::ifstream ifs(config_path);
if (ifs.is_open()) {
nlohmann::json json_config;
ifs >> json_config;
ifs.close();
bool config_changed = false;
std::vector<std::string> affected_streams;
if (json_config.contains("video_streams") &&
json_config["video_streams"].is_array()) {
for (auto& stream : json_config["video_streams"]) {
if (!stream.contains("module_config"))
continue;
auto& mc = stream["module_config"];
bool is_associated = false;
// 检查该摄像头的 light_device_ids 是否包含我们这次操作的 *任何一个* 灯
// 这样无论是单控还是群控,只要涉及到了,就关闭智能模式
std::vector<std::string> cam_lights;
if (mc.contains("light_device_ids") &&
mc["light_device_ids"].is_array()) {
cam_lights = mc["light_device_ids"].get<std::vector<std::string>>();
} else if (mc.contains("light_device_id") &&
mc["light_device_id"].is_string()) {
cam_lights.push_back(mc["light_device_id"].get<std::string>());
}
for (const auto& target_id : target_devices) {
for (const auto& cam_light_id : cam_lights) {
if (target_id == cam_light_id) {
is_associated = true;
break;
}
}
if (is_associated)
break;
}
// 如果关联且处于智能模式,强制关闭
if (is_associated) {
int current_mode = mc.value("mode", 1);
if (current_mode == 2) {
mc["mode"] = 1;
config_changed = true;
std::string stream_id = stream.value("id", "");
if (!stream_id.empty()) {
auto service = m_video_manager.getService(stream_id);
if (service) {
service->set_analysis_mode(1);
affected_streams.push_back(stream_id);
}
}
}
}
}
}
if (config_changed) {
std::ofstream ofs(config_path);
if (ofs.is_open()) {
ofs << json_config.dump(4);
ofs.close();
spdlog::info("Manual Override: Disabled Smart Mode for: {}",
fmt::join(affected_streams, ", "));
}
}
}
} catch (...) {
// 忽略非关键错误
}
crow::json::wvalue response_json;
response_json["status"] = "success";
response_json["message"] = "Command received. Executing in background.";
return crow::response(200, response_json);
});
/**
* @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();
// 简单的安全检查,防止注入
if (action != "open" && action != "close" && action != "alwaysOpen" &&
action != "alwaysClose") {
return crow::response(400,
"{\"error\":\"Invalid action. Use 'open', 'close', etc.\"}");
}
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);
}
});
/**
* @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.\"}");
});
}