diff --git a/config/alarms.json b/config/alarms.json index 51b40c0..87c2052 100644 --- a/config/alarms.json +++ b/config/alarms.json @@ -1,16 +1,4 @@ [ - { - "rule_id": "TeST", - "device_id": "rtu_temp_sensor_lab", - "data_point_name": "temperature", - "compare_type": "EQ", - "threshold": 5, - "level": "CRITICAL", - "debounce_seconds": 0, - "message_template": "测试用", - "clear_message_template": "测试解除", - "alarm_mqtt_topic": "alarms/rtu_test" - }, { "rule_id": "RTU_TEMP_HIGH_CRITICAL", "device_id": "rtu_temp_sensor_lab", diff --git a/config/devices.json b/config/devices.json index 5fa5843..b650012 100644 --- a/config/devices.json +++ b/config/devices.json @@ -34,13 +34,13 @@ "name": "count", "address": 1, "type": "INT16", - "scale": 1.0 + "scale": 1 }, { "name": "total_count", "address": 2, "type": "INT16", - "scale": 1.0 + "scale": 1 } ] }, @@ -73,7 +73,7 @@ "name": "motor_speed", "address": 100, "type": "UINT16", - "scale": 1.0 + "scale": 1 }, { "name": "pressure", @@ -85,7 +85,7 @@ "name": "valve_status", "address": 104, "type": "UINT16", - "scale": 1.0 + "scale": 1 } ] } diff --git a/config/video_config.json b/config/video_config.json new file mode 100644 index 0000000..e69de29 diff --git a/src/deviceManager/device_manager.cc b/src/deviceManager/device_manager.cc index 66691e0..549d7dd 100644 --- a/src/deviceManager/device_manager.cc +++ b/src/deviceManager/device_manager.cc @@ -2,270 +2,439 @@ #include "device_manager.h" #include "modbus/modbus_common.h" #include "spdlog/spdlog.h" -#include #include #include +#include #include +#include using json = nlohmann::json; -static ModbusDataType string_to_modbus_data_type(const std::string& type_str) { - static const std::map type_map = { - {"UINT16", ModbusDataType::UINT16}, - {"INT16", ModbusDataType::INT16}, - {"UINT32", ModbusDataType::UINT32}, - {"INT32", ModbusDataType::INT32}, - {"FLOAT32", ModbusDataType::FLOAT32} - }; - auto it = type_map.find(type_str); - if (it != type_map.end()) { - return it->second; - } - throw std::runtime_error("Unknown ModbusDataType string: " + type_str); +static ModbusDataType string_to_modbus_data_type(const std::string &type_str) { + static const std::map type_map = { + {"UINT16", ModbusDataType::UINT16}, + {"INT16", ModbusDataType::INT16}, + {"UINT32", ModbusDataType::UINT32}, + {"INT32", ModbusDataType::INT32}, + {"FLOAT32", ModbusDataType::FLOAT32}}; + auto it = type_map.find(type_str); + if (it != type_map.end()) { + return it->second; + } + throw std::runtime_error("Unknown ModbusDataType string: " + type_str); } -DeviceManager::DeviceManager(boost::asio::io_context& io_context, ReportDataCallback report_cb) - : m_io_context(io_context), m_report_callback(std::move(report_cb)) {} +DeviceManager::DeviceManager(boost::asio::io_context &io_context, + ReportDataCallback report_cb) + : m_io_context(io_context), m_report_callback(std::move(report_cb)) {} -DeviceManager::~DeviceManager() { - stop_all(); +DeviceManager::~DeviceManager() { stop_all(); } + +ModbusRtuDeviceConfig DeviceManager::_parse_rtu_config(const json &dev_json) { + ModbusRtuDeviceConfig config; + config.device_id = dev_json.at("device_id").get(); + config.port_path = dev_json.at("port_path").get(); + config.baud_rate = dev_json.at("baud_rate").get(); + config.slave_id = dev_json.at("slave_id").get(); + config.poll_interval_ms = dev_json.at("poll_interval_ms").get(); + + for (const auto &dp_json : dev_json.at("data_points")) { + config.data_points.push_back( + {dp_json.at("name").get(), + (uint16_t)dp_json.at("address").get(), + string_to_modbus_data_type(dp_json.at("type").get()), + dp_json.value("scale", 1.0)}); + } + return config; } -ModbusRtuDeviceConfig DeviceManager::_parse_rtu_config(const json& dev_json) { - ModbusRtuDeviceConfig config; - config.device_id = dev_json.at("device_id").get(); - config.port_path = dev_json.at("port_path").get(); - config.baud_rate = dev_json.at("baud_rate").get(); - config.slave_id = dev_json.at("slave_id").get(); - config.poll_interval_ms = dev_json.at("poll_interval_ms").get(); +ModbusTcpDeviceConfig DeviceManager::_parse_tcp_config(const json &dev_json) { + ModbusTcpDeviceConfig config; + config.device_id = dev_json.at("device_id").get(); + config.ip_address = dev_json.at("ip_address").get(); + config.port = dev_json.at("port").get(); + config.slave_id = dev_json.at("slave_id").get(); + config.poll_interval_ms = dev_json.at("poll_interval_ms").get(); - for (const auto& dp_json : dev_json.at("data_points")) { - config.data_points.push_back({ - dp_json.at("name").get(), - (uint16_t)dp_json.at("address").get(), - string_to_modbus_data_type(dp_json.at("type").get()), - dp_json.value("scale", 1.0) - }); - } - return config; + for (const auto &dp_json : dev_json.at("data_points")) { + config.data_points.push_back( + {dp_json.at("name").get(), + (uint16_t)dp_json.at("address").get(), + string_to_modbus_data_type(dp_json.at("type").get()), + dp_json.value("scale", 1.0)}); + } + return config; } -ModbusTcpDeviceConfig DeviceManager::_parse_tcp_config(const json& dev_json) { - ModbusTcpDeviceConfig config; - config.device_id = dev_json.at("device_id").get(); - config.ip_address = dev_json.at("ip_address").get(); - config.port = dev_json.at("port").get(); - config.slave_id = dev_json.at("slave_id").get(); - config.poll_interval_ms = dev_json.at("poll_interval_ms").get(); - - for (const auto& dp_json : dev_json.at("data_points")) { - config.data_points.push_back({ - dp_json.at("name").get(), - (uint16_t)dp_json.at("address").get(), - string_to_modbus_data_type(dp_json.at("type").get()), - dp_json.value("scale", 1.0) - }); +void DeviceManager::_validate_device_config(const nlohmann::json &config_json) { + std::set all_device_ids; + + // 辅助 lambda,用于检查 data_points + auto validate_data_points = [](const nlohmann::json &data_points_json, + const std::string &device_id) { + if (!data_points_json.is_array() || data_points_json.empty()) { + throw std::runtime_error("Device '" + device_id + + "' must have a non-empty 'data_points' array."); } - return config; + for (const auto &dp_json : data_points_json) { + dp_json.at("name").get(); + dp_json.at("address").get(); + dp_json.at("type").get(); + // .value("scale", 1.0) 是可选的, 不需检查 + } + }; + + if (config_json.contains("modbus_rtu_devices")) { + if (!config_json["modbus_rtu_devices"].is_array()) { + throw std::runtime_error("'modbus_rtu_devices' must be an array."); + } + for (const auto &dev_json : config_json["modbus_rtu_devices"]) { + if (!dev_json.is_object()) + continue; + + const auto &id = dev_json.at("device_id").get(); + dev_json.at("port_path").get(); + dev_json.at("baud_rate").get(); + dev_json.at("slave_id").get(); + dev_json.at("poll_interval_ms").get(); + + validate_data_points(dev_json.at("data_points"), id); + + if (!all_device_ids.insert(id).second) { + throw std::runtime_error("Duplicate device_id found: " + id); + } + } + } + + if (config_json.contains("modbus_tcp_devices")) { + if (!config_json["modbus_tcp_devices"].is_array()) { + throw std::runtime_error("'modbus_tcp_devices' must be an array."); + } + for (const auto &dev_json : config_json["modbus_tcp_devices"]) { + if (!dev_json.is_object()) + continue; + + const auto &id = dev_json.at("device_id").get(); + dev_json.at("ip_address").get(); + dev_json.at("port").get(); + dev_json.at("slave_id").get(); + dev_json.at("poll_interval_ms").get(); + + validate_data_points(dev_json.at("data_points"), id); + + if (!all_device_ids.insert(id).second) { + throw std::runtime_error("Duplicate device_id found: " + id); + } + } + } + + if (config_json.contains("modbus_rtu_bus_configs")) { + if (!config_json["modbus_rtu_bus_configs"].is_object()) { + throw std::runtime_error( + "'modbus_rtu_bus_configs' must be an object (map)."); + } + for (auto it = config_json["modbus_rtu_bus_configs"].begin(); + it != config_json["modbus_rtu_bus_configs"].end(); ++it) { + it.value().at("inter_device_delay_ms").get(); + } + } } +void DeviceManager::load_and_start(const std::string &config_path) { + { + std::lock_guard lock(m_config_file_mutex); + m_config_path = config_path; + } -void DeviceManager::load_and_start(const std::string& config_path) { - std::lock_guard lock(m_mutex); - spdlog::info("Loading device configuration from '{}'...", config_path); - std::ifstream config_file(config_path); - if (!config_file.is_open()) { - spdlog::critical("Failed to open configuration file: {}", config_path); - throw std::runtime_error("Configuration file not found."); - } + // 原始的启动逻辑 + std::lock_guard lock(m_mutex); + spdlog::info("Loading device configuration from '{}'...", config_path); + std::ifstream config_file(config_path); + if (!config_file.is_open()) { + spdlog::critical("Failed to open configuration file: {}", config_path); + throw std::runtime_error("Configuration file not found."); + } - try { - json config_json = json::parse(config_file); - - _parse_and_apply_config(config_json); + try { + json config_json = json::parse(config_file); - } catch (const json::exception& e) { // - spdlog::critical("Failed to parse JSON configuration: {}", e.what()); - throw; // - } + _validate_device_config(config_json); + + _parse_and_apply_config(config_json); + + } catch (const std::exception &e) { // 捕获所有错误 (parse, validate, apply) + spdlog::critical("Failed to parse, validate, or apply configuration: {}", + e.what()); + throw; + } } -void DeviceManager::post_apply_device_configuration(const std::string& json_payload) { - spdlog::debug("Posting config update task to io_context."); - boost::asio::post(m_io_context, [this, json_payload]() { - this->_apply_config_task(json_payload); - }); +void DeviceManager::post_apply_device_configuration( + const std::string &json_payload) { + spdlog::debug("Posting config update task to io_context."); + boost::asio::post(m_io_context, [this, json_payload]() { + this->_apply_config_task(json_payload); + }); } void DeviceManager::_apply_config_task(std::string json_payload) { - std::lock_guard lock(m_mutex); - spdlog::info("Applying new device configuration (from MQTT)..."); - - try { - json config_json = json::parse(json_payload); - _parse_and_apply_config(config_json); - spdlog::info("Successfully applied new device configuration."); - } catch (const json::exception& e) { - spdlog::error("Failed to parse and apply new config: {}", e.what()); - } + std::lock_guard lock(m_mutex); + spdlog::info("Applying new device configuration..."); + + try { + json config_json = json::parse(json_payload); + + // 在解析和应用之前,首先进行严格验证 + _validate_device_config(config_json); + + _parse_and_apply_config(config_json); + spdlog::info("Successfully applied new device configuration."); + } catch (const std::exception &e) { // + spdlog::error("Failed to parse, validate, and apply new config: {}", + e.what()); + } } -void DeviceManager::_parse_and_apply_config(const json& config_json) { - std::map> new_rtu_groups; - if (config_json.contains("modbus_rtu_devices")) { - for (const auto& dev_json : config_json["modbus_rtu_devices"]) { - if (!dev_json.value("enabled", false)) continue; - auto config = _parse_rtu_config(dev_json); - new_rtu_groups[config.port_path].push_back(config); - } +void DeviceManager::_parse_and_apply_config(const json &config_json) { + std::map> new_rtu_groups; + if (config_json.contains("modbus_rtu_devices")) { + for (const auto &dev_json : config_json["modbus_rtu_devices"]) { + if (!dev_json.value("enabled", false)) + continue; + auto config = _parse_rtu_config(dev_json); + new_rtu_groups[config.port_path].push_back(config); + } + } + + std::map new_tcp_configs; + if (config_json.contains("modbus_tcp_devices")) { + for (const auto &dev_json : config_json["modbus_tcp_devices"]) { + if (!dev_json.value("enabled", false)) + continue; + auto config = _parse_tcp_config(dev_json); + new_tcp_configs[config.device_id] = config; + } + } + + for (auto it = m_rtu_bus_services.begin(); it != m_rtu_bus_services.end();) { + const std::string &port_path = it->first; + if (new_rtu_groups.find(port_path) == new_rtu_groups.end()) { + spdlog::info("Config update: Stopping RTU service for port '{}'.", + port_path); + it->second->stop(); + it = m_rtu_bus_services.erase(it); + } else { + ++it; + } + } + + for (const auto &pair : new_rtu_groups) { + const std::string &port_path = pair.first; + const auto &devices_on_bus = pair.second; + + auto it = m_rtu_bus_services.find(port_path); + if (it != m_rtu_bus_services.end()) { + spdlog::info("Config update: Restarting RTU service for port '{}'...", + port_path); + it->second->stop(); + m_rtu_bus_services.erase(it); + } else { + spdlog::info("Config update: Starting new RTU service for port '{}'...", + port_path); } - std::map new_tcp_configs; - if (config_json.contains("modbus_tcp_devices")) { - for (const auto& dev_json : config_json["modbus_tcp_devices"]) { - if (!dev_json.value("enabled", false)) continue; - auto config = _parse_tcp_config(dev_json); - new_tcp_configs[config.device_id] = config; - } + auto service = std::make_unique( + port_path, devices_on_bus, m_report_callback); + service->start(); + m_rtu_bus_services[port_path] = std::move(service); + spdlog::info( + "Started Modbus RTU Bus service for port '{}' with {} device(s).", + port_path, devices_on_bus.size()); + } + + for (auto it = m_tcp_pollers.begin(); it != m_tcp_pollers.end();) { + const std::string &device_id = it->first; + if (new_tcp_configs.find(device_id) == new_tcp_configs.end()) { + spdlog::info("Config update: Stopping TCP poller for device '{}'.", + device_id); + it->second->stop(); + it = m_tcp_pollers.erase(it); + } else { + ++it; } - for (auto it = m_rtu_bus_services.begin(); it != m_rtu_bus_services.end(); ) { - const std::string& port_path = it->first; - if (new_rtu_groups.find(port_path) == new_rtu_groups.end()) { - spdlog::info("Config update: Stopping RTU service for port '{}'.", port_path); - it->second->stop(); - it = m_rtu_bus_services.erase(it); - } else { - ++it; - } + } + + for (const auto &pair : new_tcp_configs) { + const std::string &device_id = pair.first; + const auto &config = pair.second; + + auto it = m_tcp_pollers.find(device_id); + if (it != m_tcp_pollers.end()) { + spdlog::info("Config update: Restarting TCP poller for device '{}'...", + device_id); + it->second->stop(); + m_tcp_pollers.erase(it); + } else { + spdlog::info("Config update: Starting new TCP poller for device '{}'...", + device_id); } - // 启动新/更新 RTU 服务 - for (const auto& pair : new_rtu_groups) { - const std::string& port_path = pair.first; - const auto& devices_on_bus = pair.second; - - auto it = m_rtu_bus_services.find(port_path); - if (it != m_rtu_bus_services.end()) { - // TODO: 高级实现可以检查配置是否真的改变了 - spdlog::info("Config update: Restarting RTU service for port '{}'...", port_path); - it->second->stop(); - m_rtu_bus_services.erase(it); - } else { - spdlog::info("Config update: Starting new RTU service for port '{}'...", port_path); - } - - auto service = std::make_unique(port_path, devices_on_bus, m_report_callback); - service->start(); - m_rtu_bus_services[port_path] = std::move(service); - spdlog::info("Started Modbus RTU Bus service for port '{}' with {} device(s).", port_path, devices_on_bus.size()); - } - - for (auto it = m_tcp_pollers.begin(); it != m_tcp_pollers.end();) { - const std::string& device_id = it->first; - if (new_tcp_configs.find(device_id) == new_tcp_configs.end()) { - spdlog::info("Config update: Stopping TCP poller for device '{}'.", device_id); - it->second->stop(); - it = m_tcp_pollers.erase(it); - } else { - ++it; - } - } - - for (const auto& pair : new_tcp_configs) { - const std::string& device_id = pair.first; - const auto& config = pair.second; - - auto it = m_tcp_pollers.find(device_id); - if (it != m_tcp_pollers.end()) { - spdlog::info("Config update: Restarting TCP poller for device '{}'...", device_id); - it->second->stop(); - m_tcp_pollers.erase(it); - } else { - spdlog::info("Config update: Starting new TCP poller for device '{}'...", device_id); - } - - auto poller = std::make_shared(m_io_context, config, m_report_callback); - poller->start(); - m_tcp_pollers[device_id] = poller; - } + auto poller = std::make_shared(m_io_context, config, + m_report_callback); + poller->start(); + m_tcp_pollers[device_id] = poller; + } } - void DeviceManager::stop_all() { - std::lock_guard lock(m_mutex); - spdlog::info("Stopping all device services..."); - - for (auto& pair : m_rtu_bus_services) { - pair.second->stop(); - } - m_rtu_bus_services.clear(); - - for (auto& pair : m_tcp_pollers) { - pair.second->stop(); - } - m_tcp_pollers.clear(); + std::lock_guard lock(m_mutex); + spdlog::info("Stopping all device services..."); - - spdlog::info("All device services stopped."); + for (auto &pair : m_rtu_bus_services) { + pair.second->stop(); + } + m_rtu_bus_services.clear(); + + for (auto &pair : m_tcp_pollers) { + pair.second->stop(); + } + m_tcp_pollers.clear(); + + spdlog::info("All device services stopped."); } std::vector DeviceManager::get_all_device_info() const { - std::lock_guard lock(m_mutex); + std::lock_guard lock(m_mutex); - std::vector all_devices; - - for (const auto& pair : m_rtu_bus_services) { - const auto& service = pair.second; - const auto& configs_on_bus = service->get_all_device_configs(); - for (const auto& config : configs_on_bus) { - DeviceInfo info; - info.id = config.device_id; - info.type = "ModbusRTU"; - info.is_running = service->is_running(); - info.connection_details["Port Path"] = config.port_path; - info.connection_details["Baud Rate"] = std::to_string(config.baud_rate); - info.connection_details["Slave ID"] = std::to_string(config.slave_id); - all_devices.push_back(info); - } + std::vector all_devices; + + for (const auto &pair : m_rtu_bus_services) { + const auto &service = pair.second; + const auto &configs_on_bus = service->get_all_device_configs(); + for (const auto &config : configs_on_bus) { + DeviceInfo info; + info.id = config.device_id; + info.type = "ModbusRTU"; + info.is_running = service->is_running(); + info.connection_details["Port Path"] = config.port_path; + info.connection_details["Baud Rate"] = std::to_string(config.baud_rate); + info.connection_details["Slave ID"] = std::to_string(config.slave_id); + all_devices.push_back(info); } + } - for (const auto& pair : m_tcp_pollers) { - const auto& poller = pair.second; - const auto& config = poller->get_config(); + for (const auto &pair : m_tcp_pollers) { + const auto &poller = pair.second; + const auto &config = poller->get_config(); - DeviceInfo info; - info.id = config.device_id; - info.type = "ModbusTCP"; - info.is_running = poller->is_running(); - info.connection_details["IP Address"] = config.ip_address; - info.connection_details["Port"] = std::to_string(config.port); - info.connection_details["Slave ID"] = std::to_string(config.slave_id); - all_devices.push_back(info); - } + DeviceInfo info; + info.id = config.device_id; + info.type = "ModbusTCP"; + info.is_running = poller->is_running(); + info.connection_details["IP Address"] = config.ip_address; + info.connection_details["Port"] = std::to_string(config.port); + info.connection_details["Slave ID"] = std::to_string(config.slave_id); + all_devices.push_back(info); + } - - return all_devices; + return all_devices; } -bool DeviceManager::send_control_command(const std::string& device_id, uint16_t address, uint16_t value) { - std::lock_guard lock(m_mutex); +bool DeviceManager::send_control_command(const std::string &device_id, + uint16_t address, uint16_t value) { + std::lock_guard lock(m_mutex); - auto tcp_it = m_tcp_pollers.find(device_id); - if (tcp_it != m_tcp_pollers.end()) { - spdlog::info("Found TCP device '{}'. Dispatching write command to address {}.", device_id, address); - tcp_it->second->write_single_register(address, value); - return true; + auto tcp_it = m_tcp_pollers.find(device_id); + if (tcp_it != m_tcp_pollers.end()) { + spdlog::info( + "Found TCP device '{}'. Dispatching write command to address {}.", + device_id, address); + tcp_it->second->write_single_register(address, value); + return true; + } + + for (const auto &pair : m_rtu_bus_services) { + const auto &service = pair.second; + if (service->manages_device(device_id)) { + spdlog::info("Found RTU device '{}' on a bus. Dispatching write command.", + device_id); + service->write_single_register(device_id, address, value); + return true; } + } - for (const auto& pair : m_rtu_bus_services) { - const auto& service = pair.second; - if (service->manages_device(device_id)) { - spdlog::info("Found RTU device '{}' on a bus. Dispatching write command.", device_id); - service->write_single_register(device_id, address, value); - return true; - } - } + spdlog::warn("send_control_command failed: Device with ID '{}' not found in " + "any service.", + device_id); + return false; +} - spdlog::warn("send_control_command failed: Device with ID '{}' not found in any service.", device_id); // - return false; +std::string DeviceManager::get_config_as_json_string() const { + std::lock_guard lock(m_config_file_mutex); + if (m_config_path.empty()) { + spdlog::warn("m_config_path is empty, cannot read config."); + return "{}"; + } + + std::ifstream config_file(m_config_path); + if (!config_file.is_open()) { + spdlog::error("Failed to open configuration file for reading: {}", + m_config_path); + return "{}"; + } + + std::stringstream buffer; + buffer << config_file.rdbuf(); + return buffer.str(); +} + +bool DeviceManager::save_config_from_json_string( + const std::string &json_content) { + std::lock_guard lock(m_config_file_mutex); + if (m_config_path.empty()) { + spdlog::error("m_config_path is empty, cannot save config."); + return false; + } + + // 1. 验证 JSON 格式和 Schema + try { + json config_json = json::parse(json_content); + + // 在写入文件之前,确保配置是有效的 + _validate_device_config(config_json); + + } catch (const std::exception &e) { // 捕获 json::parse 和 _validate 的异常 + spdlog::error("Failed to save config: Invalid JSON format or schema. {}", + e.what()); + return false; // 返回 false, web_server 会返回 400 + } + + // 2. 写入文件 + std::ofstream config_file(m_config_path); + if (!config_file.is_open()) { + spdlog::error("Failed to open configuration file for writing: {}", + m_config_path); + return false; + } + + config_file << json_content; + spdlog::info("Successfully saved new device configuration to {}", + m_config_path); + return true; +} + +bool DeviceManager::reload_config_from_file() { + spdlog::info("Posting task to reload device config from file..."); + std::string config_content; + try { + // 必须先读取,因为 post_apply_device_configuration 需要 payload + config_content = get_config_as_json_string(); + } catch (const std::exception &e) { + spdlog::error("Failed to read config file for reload: {}", e.what()); + return false; + } + + // 复用现有的、线程安全的配置应用方法 + post_apply_device_configuration(config_content); + return true; } \ No newline at end of file diff --git a/src/deviceManager/device_manager.h b/src/deviceManager/device_manager.h index 7ef0324..c6583a7 100644 --- a/src/deviceManager/device_manager.h +++ b/src/deviceManager/device_manager.h @@ -2,25 +2,25 @@ #ifndef DEVICE_MANAGER_H #define DEVICE_MANAGER_H -#include "protocol/iprotocol_adapter.h" -#include "modbus/modbus_rtu_bus_service.h" #include "modbus/modbus_master_poller.h" +#include "modbus/modbus_rtu_bus_service.h" +#include "protocol/iprotocol_adapter.h" #include -#include +#include #include -#include #include -#include -#include // +#include +#include +#include /** * @brief 用于向API层传递设备信息的统一结构体 */ struct DeviceInfo { - std::string id; - std::string type; - bool is_running; - std::map connection_details; + std::string id; + std::string type; + bool is_running; + std::map connection_details; }; /** @@ -30,76 +30,109 @@ struct DeviceInfo { */ class DeviceManager { public: - DeviceManager(boost::asio::io_context& io_context, ReportDataCallback report_cb); - ~DeviceManager(); + DeviceManager(boost::asio::io_context &io_context, + ReportDataCallback report_cb); + ~DeviceManager(); - // 禁止拷贝和赋值 - DeviceManager(const DeviceManager&) = delete; - DeviceManager& operator=(const DeviceManager&) = delete; + // 禁止拷贝和赋值 + DeviceManager(const DeviceManager &) = delete; + DeviceManager &operator=(const DeviceManager &) = delete; - /** - * @brief 从JSON配置文件加载所有设备并启动服务 - * @param config_path JSON配置文件的路径 - */ - void load_and_start(const std::string& config_path); // + /** + * @brief [修改] 从JSON配置文件加载所有设备并启动服务 + * @param config_path JSON配置文件的路径 + */ + void load_and_start(const std::string &config_path); - /** - * @brief (线程安全) 从 MQTT 线程接收配置更新请求 - * 此方法是异步的,它将配置应用任务 post 到 io_context 线程。 - * @param json_payload 包含新配置的 JSON 字符串 - */ - void post_apply_device_configuration(const std::string& json_payload); + /** + * @brief (线程安全) 从 MQTT 线程接收配置更新请求 + * 此方法是异步的,它将配置应用任务 post 到 io_context 线程。 + * @param json_payload 包含新配置的 JSON 字符串 + */ + void post_apply_device_configuration(const std::string &json_payload); - /** - * @brief 获取所有当前管理的设备的信息 - * @return 包含所有设备信息的vector - */ - std::vector get_all_device_info() const; // + /** + * @brief 获取所有当前管理的设备的信息 + * @return 包含所有设备信息的vector + */ + std::vector get_all_device_info() const; - /** - * @brief 安全地停止所有正在运行的设备服务 - */ - void stop_all(); // + /** + * @brief 安全地停止所有正在运行的设备服务 + */ + void stop_all(); - /** - * @brief 向指定的Modbus设备发送一个写单个寄存器的命令 - * @param device_id 目标设备的唯一ID - * @param address 要写入的寄存器地址 - * @param value 要写入的值 - * @return 如果找到设备并成功分派命令,则返回true;否则返回false - */ - bool send_control_command(const std::string& device_id, uint16_t address, uint16_t value); // + /** + * @brief 向指定的Modbus设备发送一个写单个寄存器的命令 + * @param device_id 目标设备的唯一ID + * @param address 要写入的寄存器地址 + * @param value 要写入的值 + * @return 如果找到设备并成功分派命令,则返回true;否则返回false + */ + bool send_control_command(const std::string &device_id, uint16_t address, + uint16_t value); + + /** + * @brief (线程安全) 获取当前设备配置文件的内容 + * @return 配置文件内容的 JSON 字符串 + */ + std::string get_config_as_json_string() const; + + /** + * @brief (线程安全) 将新的 JSON 内容保存回配置文件 + * @param json_content 要写入的 JSON 字符串 + * @return 成功返回 true, 失败 (如 JSON 格式或 schema 无效) 返回 false + */ + bool save_config_from_json_string(const std::string &json_content); + + /** + * @brief (线程安全) 重新加载配置文件并应用 + * (复用现有的 post_apply... 逻辑) + * @return 成功返回 true + */ + bool reload_config_from_file(); private: - /** - * @brief (私有, 在 io_context 线程上执行) - * 锁定互斥锁并执行配置应用。 - */ - void _apply_config_task(std::string json_payload); + /** + * @brief (私有, 在 io_context 线程上执行) + * 锁定互斥锁并执行配置应用。 + */ + void _apply_config_task(std::string json_payload); - /** - * @brief (私有, 必须在互斥锁保护下调用) - * 核心的 "Diff" 逻辑,比较新配置与当前运行的服务,并应用更改。 - */ - void _parse_and_apply_config(const nlohmann::json& config_json); + /** + * @brief (私有, 必须在互斥锁保护下调用) + * 核心的 "Diff" 逻辑,比较新配置与当前运行的服务,并应用更改。 + */ + void _parse_and_apply_config(const nlohmann::json &config_json); - /** - * @brief (私有, 辅助函数) 解析 Modbus RTU 设备配置 - */ - ModbusRtuDeviceConfig _parse_rtu_config(const nlohmann::json& dev_json); + /** + * @brief (私有) 验证配置 JSON 的 schema 和 device_id 唯一性 + * @param config_json 已解析的 nlohmann::json 对象 + * @throws std::runtime_error 如果验证失败 + */ + void _validate_device_config(const nlohmann::json &config_json); - /** - * @brief (私有, 辅助函数) 解析 Modbus TCP 设备配置 - */ - ModbusTcpDeviceConfig _parse_tcp_config(const nlohmann::json& dev_json); + /** + * @brief (私有, 辅助函数) 解析 Modbus RTU 设备配置 + */ + ModbusRtuDeviceConfig _parse_rtu_config(const nlohmann::json &dev_json); + + /** + * @brief (私有, 辅助函数) 解析 Modbus TCP 设备配置 + */ + ModbusTcpDeviceConfig _parse_tcp_config(const nlohmann::json &dev_json); private: - boost::asio::io_context& m_io_context; - ReportDataCallback m_report_callback; + boost::asio::io_context &m_io_context; + ReportDataCallback m_report_callback; - std::map> m_rtu_bus_services; - std::map> m_tcp_pollers; - mutable std::mutex m_mutex; + std::map> + m_rtu_bus_services; + std::map> m_tcp_pollers; + mutable std::mutex m_mutex; + + std::string m_config_path; // 存储配置文件的路径 + mutable std::mutex m_config_file_mutex; // 保护配置文件读写的专用互斥锁 }; #endif // DEVICE_MANAGER_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index bcc97cd..12a7a5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,10 +28,11 @@ boost::asio::io_context g_io_context; /** * @brief 周期性轮询系统状态并发布到 MQTT */ -void poll_system_metrics(boost::asio::steady_timer& timer, - SystemMonitor::SystemMonitor& monitor, - MqttClient& mqtt_client, AlarmService& alarm_service) { - if (g_io_context.stopped()) return; +void poll_system_metrics(boost::asio::steady_timer &timer, + SystemMonitor::SystemMonitor &monitor, + MqttClient &mqtt_client, AlarmService &alarm_service) { + if (g_io_context.stopped()) + return; auto cpu_util = monitor.getCpuUtilization(); auto mem_info = monitor.getMemoryInfo(); double mem_total_gb = mem_info.total_kb / 1024.0 / 1024.0; @@ -56,7 +57,7 @@ void poll_system_metrics(boost::asio::steady_timer& timer, payload = payload_json.dump(); - } catch (const nlohmann::json::parse_error& e) { + } catch (const nlohmann::json::parse_error &e) { spdlog::error("Failed to parse thermalInfo JSON: {}. Sending partial data.", e.what()); nlohmann::json fallback_json; @@ -80,7 +81,7 @@ void poll_system_metrics(boost::asio::steady_timer& timer, std::ref(alarm_service))); } -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { const std::string config_path = "/app/config/config.json"; if (!ConfigManager::getInstance().load(config_path)) { std::cerr << "Failed to load configuration from " << config_path @@ -88,18 +89,18 @@ int main(int argc, char* argv[]) { << std::endl; } - auto& config = ConfigManager::getInstance(); + auto &config = ConfigManager::getInstance(); try { spdlog::set_level(spdlog::level::from_str(config.getLogLevel())); spdlog::info("Edge Proxy starting up..."); - } catch (const spdlog::spdlog_ex& ex) { + } catch (const spdlog::spdlog_ex &ex) { std::cerr << "Log initialization failed: " << ex.what() << std::endl; return 1; } spdlog::info("Initializing Data Storage..."); - auto& data_storage = DataStorage::getInstance(); + auto &data_storage = DataStorage::getInstance(); if (!data_storage.initialize(config.getDataStorageDbPath())) { spdlog::critical("Failed to initialize DataStorage. Exiting."); return 1; @@ -122,7 +123,7 @@ int main(int argc, char* argv[]) { spdlog::error("Failed to load alarm rules. Alarms may be disabled."); } - auto report_to_mqtt = [&](const UnifiedData& data) { + auto report_to_mqtt = [&](const UnifiedData &data) { if (data_storage.storeProcessedData(data)) { spdlog::debug("Successfully stored PROCESSED data for device '{}'", data.device_id); @@ -161,7 +162,7 @@ int main(int argc, char* argv[]) { } CacheUploader cache_uploader(g_io_context, mqtt_client, data_cache); - mqtt_client.set_connected_handler([&](const std::string& cause) { + mqtt_client.set_connected_handler([&](const std::string &cause) { spdlog::info("MQTT client connected: {}", cause); cache_uploader.start_upload(); }); @@ -190,7 +191,7 @@ int main(int argc, char* argv[]) { boost::asio::signal_set signals(g_io_context, SIGINT, SIGTERM); signals.async_wait( - [&](const boost::system::error_code& error, int signal_number) { + [&](const boost::system::error_code &error, int signal_number) { spdlog::warn("Interrupt signal ({}) received. Shutting down.", signal_number); @@ -222,7 +223,7 @@ int main(int argc, char* argv[]) { spdlog::info("All services are running. Press Ctrl+C to exit."); g_io_context.run(); - } catch (const std::exception& e) { + } catch (const std::exception &e) { spdlog::critical("An unhandled exception occurred: {}", e.what()); return 1; } diff --git a/src/web/web_server.cc b/src/web/web_server.cc index 8d5bff2..8c18bd7 100644 --- a/src/web/web_server.cc +++ b/src/web/web_server.cc @@ -89,8 +89,9 @@ void WebServer::setup_routes() { devices_json.push_back(std::move(device_obj)); } - - return crow::json::wvalue(devices_json); + 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] { @@ -100,8 +101,12 @@ void WebServer::setup_routes() { for (const auto &pair : latest_data_map) { response[pair.first] = crow::json::load(pair.second); } - - return response; + // 1. 将 JSON 对象转换为 crow::response + auto res = crow::response(response); + // 2. 明确设置 Content-Type 头部 + res.set_header("Content-Type", "application/json"); + // 3. 返回 response 对象 + return res; }); CROW_ROUTE((*this), "/api/alarms/active").methods("GET"_method)([this] { @@ -252,4 +257,81 @@ void WebServer::setup_routes() { 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; + } + }); } \ No newline at end of file