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