diff --git a/CMakeLists.txt b/CMakeLists.txt index 81602171..49afd7c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -273,6 +273,7 @@ list(APPEND trunk_recorder_sources trunk-recorder/sources/iq_file_source.cc trunk-recorder/csv_helper.cc trunk-recorder/config.cc + trunk-recorder/config_service.cc trunk-recorder/setup_systems.cc trunk-recorder/monitor_systems.cc trunk-recorder/talkgroup.cc diff --git a/trunk-recorder/config.cc b/trunk-recorder/config.cc index 771cec60..c067c25b 100644 --- a/trunk-recorder/config.cc +++ b/trunk-recorder/config.cc @@ -117,6 +117,9 @@ bool load_config(string config_file, Config &config, gr::top_block_sptr &tb, std << "byte position of error: " << e.byte << std::endl; } + // Store the raw parsed JSON so we can track which keys were explicitly set + config.loaded_json = data; + try { // const std::string json_filename = "config.json"; @@ -246,11 +249,13 @@ bool load_config(string config_file, Config &config, gr::top_block_sptr &tb, std BOOST_LOG_TRIVIAL(info) << "\n-------------------------------------\nSYSTEMS\n-------------------------------------\n"; + int sys_json_index = 0; for (json element : data["systems"]) { bool system_enabled = element.value("enabled", true); if (system_enabled) { // each system should have a unique index value; System *system = System::make(sys_count++); + system->set_config_index(sys_json_index); std::stringstream default_script; unsigned long sys_id; @@ -460,9 +465,11 @@ bool load_config(string config_file, Config &config, gr::top_block_sptr &tb, std systems.push_back(system); BOOST_LOG_TRIVIAL(info); } + sys_json_index++; } BOOST_LOG_TRIVIAL(info) << "\n\n-------------------------------------\nSOURCES\n-------------------------------------\n"; + int source_json_index = 0; for (json element : data["sources"]) { bool source_enabled = element.value("enabled", true); @@ -651,10 +658,12 @@ bool load_config(string config_file, Config &config, gr::top_block_sptr &tb, std source->create_debug_recorder(tb, source_count); } + source->set_config_index(source_json_index); sources.push_back(source); source_count++; BOOST_LOG_TRIVIAL(info) << "\n-------------------------------------\n\n"; } + source_json_index++; } BOOST_LOG_TRIVIAL(info) << "\n\n-------------------------------------\nPLUGINS\n-------------------------------------\n"; @@ -679,3 +688,38 @@ bool load_config(string config_file, Config &config, gr::top_block_sptr &tb, std BOOST_LOG_TRIVIAL(info) << "\n\n"; return true; } + +bool save_config(const Config &config) { + if (config.config_file.empty()) { + BOOST_LOG_TRIVIAL(error) << "save_config: No config file path set"; + return false; + } + + try { + // Create a backup of the current config file with a timestamp + if (boost::filesystem::exists(config.config_file)) { + auto now = std::chrono::system_clock::now(); + auto time_t_now = std::chrono::system_clock::to_time_t(now); + std::tm tm_now; + localtime_r(&time_t_now, &tm_now); + char time_buf[32]; + std::strftime(time_buf, sizeof(time_buf), "%Y%m%d_%H%M%S", &tm_now); + + std::string backup_file = config.config_file + "." + time_buf + ".bak"; + boost::filesystem::copy_file(config.config_file, backup_file); + BOOST_LOG_TRIVIAL(info) << "Configuration backup saved to: " << backup_file; + } + + std::ofstream f(config.config_file); + if (!f.is_open()) { + BOOST_LOG_TRIVIAL(error) << "save_config: Could not open file for writing: " << config.config_file; + return false; + } + f << config.loaded_json.dump(4) << std::endl; + BOOST_LOG_TRIVIAL(info) << "Configuration saved to: " << config.config_file; + return true; + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(error) << "save_config: Failed to save config: " << e.what(); + return false; + } +} diff --git a/trunk-recorder/config.h b/trunk-recorder/config.h index 1e0ea0b4..d10db887 100644 --- a/trunk-recorder/config.h +++ b/trunk-recorder/config.h @@ -34,5 +34,6 @@ #include bool load_config(std::string config_file, Config &config, gr::top_block_sptr &tb, std::vector &sources, std::vector &systems); +bool save_config(const Config &config); #endif \ No newline at end of file diff --git a/trunk-recorder/config_service.cc b/trunk-recorder/config_service.cc new file mode 100644 index 00000000..e9b69e26 --- /dev/null +++ b/trunk-recorder/config_service.cc @@ -0,0 +1,879 @@ +#include "config_service.h" +#include "config.h" +#include "formatter.h" +#include "source.h" +#include "systems/system.h" + +#include +#include + +// Global configuration service instance +ConfigurationService* g_config_service = nullptr; + +void init_config_service(Config* config, std::vector* sources, std::vector* systems) { + if (g_config_service == nullptr) { + g_config_service = new ConfigurationService(); + } + g_config_service->init(config, sources, systems); +} + +void shutdown_config_service() { + if (g_config_service != nullptr) { + delete g_config_service; + g_config_service = nullptr; + } +} + +ConfigurationService::ConfigurationService() + : m_config(nullptr), m_sources(nullptr), m_systems(nullptr), m_initialized(false) { +} + +ConfigurationService::~ConfigurationService() { +} + +void ConfigurationService::init(Config* config, std::vector* sources, std::vector* systems) { + m_config = config; + m_sources = sources; + m_systems = systems; + m_initialized = true; + BOOST_LOG_TRIVIAL(info) << "ConfigurationService initialized with " << sources->size() << " sources and " << systems->size() << " systems"; +} + +// ============================================================ +// Command submission +// ============================================================ + +bool ConfigurationService::submit_command(const ConfigCommand& cmd) { + if (!m_initialized) { + BOOST_LOG_TRIVIAL(error) << "ConfigurationService: Cannot submit command - not initialized"; + return false; + } + + std::lock_guard lock(m_queue_mutex); + m_pending_commands.push(cmd); + return true; +} + +// Convenience methods for Source parameters + +bool ConfigurationService::set_source_gain(int source_num, double gain, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_GAIN; + cmd.target_id = source_num; + cmd.value = gain; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_source_gain_by_name(int source_num, const std::string& name, double gain, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_GAIN_BY_NAME; + cmd.target_id = source_num; + cmd.param_name = name; + cmd.value = gain; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_source_error(int source_num, double error, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_ERROR; + cmd.target_id = source_num; + cmd.value = error; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_source_ppm(int source_num, double ppm, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_PPM; + cmd.target_id = source_num; + cmd.value = ppm; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_source_gain_mode(int source_num, bool agc, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_GAIN_MODE; + cmd.target_id = source_num; + cmd.value = agc; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_source_antenna(int source_num, const std::string& antenna, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_ANTENNA; + cmd.target_id = source_num; + cmd.value = antenna; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_source_signal_detector_threshold(int source_num, double threshold, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD; + cmd.target_id = source_num; + cmd.value = threshold; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +// Convenience methods for System parameters + +bool ConfigurationService::set_system_squelch(int sys_num, double squelch_db, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_SQUELCH_DB; + cmd.target_id = sys_num; + cmd.value = squelch_db; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_analog_levels(int sys_num, double levels, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_ANALOG_LEVELS; + cmd.target_id = sys_num; + cmd.value = levels; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_digital_levels(int sys_num, double levels, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_DIGITAL_LEVELS; + cmd.target_id = sys_num; + cmd.value = levels; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_min_duration(int sys_num, double duration, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_MIN_DURATION; + cmd.target_id = sys_num; + cmd.value = duration; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_max_duration(int sys_num, double duration, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_MAX_DURATION; + cmd.target_id = sys_num; + cmd.value = duration; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_min_tx_duration(int sys_num, double duration, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_MIN_TX_DURATION; + cmd.target_id = sys_num; + cmd.value = duration; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_record_unknown(int sys_num, bool record, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_RECORD_UNKNOWN; + cmd.target_id = sys_num; + cmd.value = record; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_hide_encrypted(int sys_num, bool hide, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_HIDE_ENCRYPTED; + cmd.target_id = sys_num; + cmd.value = hide; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_hide_unknown(int sys_num, bool hide, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_HIDE_UNKNOWN; + cmd.target_id = sys_num; + cmd.value = hide; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_conversation_mode(int sys_num, bool mode, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_CONVERSATION_MODE; + cmd.target_id = sys_num; + cmd.value = mode; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_tau(int sys_num, float tau, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_TAU; + cmd.target_id = sys_num; + cmd.value = static_cast(tau); + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_max_dev(int sys_num, int max_dev, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_MAX_DEV; + cmd.target_id = sys_num; + cmd.value = max_dev; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_system_filter_width(int sys_num, double width, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_SYSTEM_FILTER_WIDTH; + cmd.target_id = sys_num; + cmd.value = width; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::set_call_timeout(double timeout, const std::string& requester, ConfigCallback callback) { + ConfigCommand cmd; + cmd.type = ConfigCommandType::SET_CALL_TIMEOUT; + cmd.target_id = -1; // Global config + cmd.value = timeout; + cmd.requester = requester; + cmd.callback = callback; + return submit_command(cmd); +} + +bool ConfigurationService::save_config() { + if (!m_config) { + BOOST_LOG_TRIVIAL(error) << "ConfigurationService: Cannot save config - not initialized"; + return false; + } + return ::save_config(*m_config); +} + +// ============================================================ +// Getters +// ============================================================ + +Source* ConfigurationService::find_source(int source_num) { + if (!m_sources) return nullptr; + for (Source* src : *m_sources) { + if (src->get_num() == source_num) { + return src; + } + } + return nullptr; +} + +System* ConfigurationService::find_system(int sys_num) { + if (!m_systems) return nullptr; + for (System* sys : *m_systems) { + if (sys->get_sys_num() == sys_num) { + return sys; + } + } + return nullptr; +} + +Source* ConfigurationService::get_source(int source_num) { + return find_source(source_num); +} + +System* ConfigurationService::get_system(int sys_num) { + return find_system(sys_num); +} + +System* ConfigurationService::get_system_by_short_name(const std::string& short_name) { + if (!m_systems) return nullptr; + for (System* sys : *m_systems) { + if (sys->get_short_name() == short_name) { + return sys; + } + } + return nullptr; +} + +std::optional ConfigurationService::get_source_gain(int source_num) { + Source* src = find_source(source_num); + if (!src) return std::nullopt; + return src->get_gain(); +} + +std::optional ConfigurationService::get_source_error(int source_num) { + Source* src = find_source(source_num); + if (!src) return std::nullopt; + return src->get_error(); +} + +std::optional ConfigurationService::get_source_gain_mode(int source_num) { + Source* src = find_source(source_num); + if (!src) return std::nullopt; + return src->get_gain_mode(); +} + +std::optional ConfigurationService::get_source_antenna(int source_num) { + Source* src = find_source(source_num); + if (!src) return std::nullopt; + return src->get_antenna(); +} + +std::optional ConfigurationService::get_system_squelch(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_squelch_db(); +} + +std::optional ConfigurationService::get_system_analog_levels(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_analog_levels(); +} + +std::optional ConfigurationService::get_system_digital_levels(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_digital_levels(); +} + +std::optional ConfigurationService::get_system_min_duration(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_min_duration(); +} + +std::optional ConfigurationService::get_system_max_duration(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_max_duration(); +} + +std::optional ConfigurationService::get_system_record_unknown(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_record_unknown(); +} + +std::optional ConfigurationService::get_system_conversation_mode(int sys_num) { + System* sys = find_system(sys_num); + if (!sys) return std::nullopt; + return sys->get_conversation_mode(); +} + +double ConfigurationService::get_call_timeout() { + if (!m_config) return 3.0; // Default + return m_config->call_timeout; +} + +// ============================================================ +// Change notification +// ============================================================ + +void ConfigurationService::register_change_listener(ConfigChangeListener listener) { + std::lock_guard lock(m_listeners_mutex); + m_change_listeners.push_back(listener); +} + +void ConfigurationService::notify_listeners(const ConfigChangeInfo& change) { + std::lock_guard lock(m_listeners_mutex); + for (auto& listener : m_change_listeners) { + try { + listener(change); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "ConfigurationService: Listener threw exception: " << e.what(); + } + } +} + +// ============================================================ +// Validation +// ============================================================ + +ConfigResult ConfigurationService::validate_command(const ConfigCommand& cmd) { + // Validate target exists + if (cmd.type >= ConfigCommandType::SET_SOURCE_GAIN && cmd.type <= ConfigCommandType::SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD) { + if (!find_source(cmd.target_id)) { + return ConfigResult::INVALID_TARGET; + } + } else if (cmd.type >= ConfigCommandType::SET_SYSTEM_SQUELCH_DB && cmd.type <= ConfigCommandType::SET_SYSTEM_FILTER_WIDTH) { + if (!find_system(cmd.target_id)) { + return ConfigResult::INVALID_TARGET; + } + } + + // Validate value ranges based on command type + switch (cmd.type) { + case ConfigCommandType::SET_SOURCE_GAIN: + case ConfigCommandType::SET_SOURCE_GAIN_BY_NAME: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double gain = std::get(cmd.value); + if (gain < 0 || gain > 100) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SOURCE_ERROR: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + // Error can be positive or negative, reasonable range check + double error = std::get(cmd.value); + if (error < -1000000 || error > 1000000) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SOURCE_PPM: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double ppm = std::get(cmd.value); + if (ppm < -1000 || ppm > 1000) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SOURCE_GAIN_MODE: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + break; + } + case ConfigCommandType::SET_SOURCE_ANTENNA: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + break; + } + case ConfigCommandType::SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double threshold = std::get(cmd.value); + if (threshold < -200 || threshold > 0) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SYSTEM_SQUELCH_DB: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double squelch = std::get(cmd.value); + if (squelch < -200 || squelch > 0) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SYSTEM_ANALOG_LEVELS: + case ConfigCommandType::SET_SYSTEM_DIGITAL_LEVELS: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double levels = std::get(cmd.value); + if (levels < 0 || levels > 100) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SYSTEM_MIN_DURATION: + case ConfigCommandType::SET_SYSTEM_MAX_DURATION: + case ConfigCommandType::SET_SYSTEM_MIN_TX_DURATION: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double duration = std::get(cmd.value); + if (duration < 0 || duration > 86400) return ConfigResult::INVALID_VALUE; // Max 24 hours + break; + } + case ConfigCommandType::SET_SYSTEM_RECORD_UNKNOWN: + case ConfigCommandType::SET_SYSTEM_HIDE_ENCRYPTED: + case ConfigCommandType::SET_SYSTEM_HIDE_UNKNOWN: + case ConfigCommandType::SET_SYSTEM_CONVERSATION_MODE: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + break; + } + case ConfigCommandType::SET_SYSTEM_TAU: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double tau = std::get(cmd.value); + if (tau < 0 || tau > 1) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SYSTEM_MAX_DEV: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + int max_dev = std::get(cmd.value); + if (max_dev < 0 || max_dev > 50000) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_SYSTEM_FILTER_WIDTH: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double width = std::get(cmd.value); + if (width < 0.1 || width > 10) return ConfigResult::INVALID_VALUE; + break; + } + case ConfigCommandType::SET_CALL_TIMEOUT: { + if (!std::holds_alternative(cmd.value)) return ConfigResult::TYPE_MISMATCH; + double timeout = std::get(cmd.value); + if (timeout < 0.1 || timeout > 300) return ConfigResult::INVALID_VALUE; + break; + } + } + + return ConfigResult::SUCCESS; +} + +// ============================================================ +// loaded_json update helpers +// ============================================================ + +void ConfigurationService::update_source_json(Source* src, const std::string& json_key, const ConfigValue& value) { + if (!m_config || src->get_config_index() < 0) return; + auto& sources_json = m_config->loaded_json["sources"]; + if (src->get_config_index() >= static_cast(sources_json.size())) return; + std::visit([&](const auto& v) { sources_json[src->get_config_index()][json_key] = v; }, value); +} + +void ConfigurationService::update_system_json(System* sys, const std::string& json_key, const ConfigValue& value) { + if (!m_config || sys->get_config_index() < 0) return; + auto& systems_json = m_config->loaded_json["systems"]; + if (sys->get_config_index() >= static_cast(systems_json.size())) return; + std::visit([&](const auto& v) { systems_json[sys->get_config_index()][json_key] = v; }, value); +} + +void ConfigurationService::update_config_json(const std::string& json_key, const ConfigValue& value) { + if (!m_config) return; + std::visit([&](const auto& v) { m_config->loaded_json[json_key] = v; }, value); +} + +// ============================================================ +// Command execution +// ============================================================ + +ConfigResult ConfigurationService::execute_command(const ConfigCommand& cmd, ConfigValue& old_value) { + switch (cmd.type) { + // Source commands + case ConfigCommandType::SET_SOURCE_GAIN: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = src->get_gain(); + src->set_gain(std::get(cmd.value)); + update_source_json(src, "gain", cmd.value); + break; + } + case ConfigCommandType::SET_SOURCE_GAIN_BY_NAME: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = static_cast(src->get_gain_by_name(cmd.param_name)); + src->set_gain_by_name(cmd.param_name, std::get(cmd.value)); + // Map gain name to JSON key (e.g., "IF" -> "ifGain", "BB" -> "bbGain") + static const std::map gain_name_to_json = { + {"IF", "ifGain"}, {"BB", "bbGain"}, {"MIX", "mixGain"}, {"LNA", "lnaGain"}, + {"TIA", "tiaGain"}, {"PGA", "pgaGain"}, {"AMP", "ampGain"}, {"VGA", "vgaGain"}, + {"VGA1", "vga1Gain"}, {"VGA2", "vga2Gain"} + }; + auto it = gain_name_to_json.find(cmd.param_name); + if (it != gain_name_to_json.end()) { + update_source_json(src, it->second, cmd.value); + } + break; + } + case ConfigCommandType::SET_SOURCE_ERROR: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = src->get_error(); + src->set_error(std::get(cmd.value)); + update_source_json(src, "error", cmd.value); + break; + } + case ConfigCommandType::SET_SOURCE_PPM: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = 0.0; // No getter for PPM + src->set_freq_corr(std::get(cmd.value)); + update_source_json(src, "ppm", cmd.value); + break; + } + case ConfigCommandType::SET_SOURCE_GAIN_MODE: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = src->get_gain_mode(); + src->set_gain_mode(std::get(cmd.value)); + update_source_json(src, "agc", cmd.value); + break; + } + case ConfigCommandType::SET_SOURCE_ANTENNA: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = src->get_antenna(); + src->set_antenna(std::get(cmd.value)); + update_source_json(src, "antenna", cmd.value); + break; + } + case ConfigCommandType::SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD: { + Source* src = find_source(cmd.target_id); + if (!src) return ConfigResult::INVALID_TARGET; + old_value = 0.0; // No getter currently + src->set_signal_detector_threshold(static_cast(std::get(cmd.value))); + update_source_json(src, "signalDetectorThreshold", cmd.value); + break; + } + + // System commands + case ConfigCommandType::SET_SYSTEM_SQUELCH_DB: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_squelch_db(); + sys->set_squelch_db(std::get(cmd.value)); + update_system_json(sys, "squelch", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_ANALOG_LEVELS: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_analog_levels(); + sys->set_analog_levels(std::get(cmd.value)); + update_system_json(sys, "analogLevels", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_DIGITAL_LEVELS: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_digital_levels(); + sys->set_digital_levels(std::get(cmd.value)); + update_system_json(sys, "digitalLevels", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_MIN_DURATION: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_min_duration(); + sys->set_min_duration(std::get(cmd.value)); + update_system_json(sys, "minDuration", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_MAX_DURATION: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_max_duration(); + sys->set_max_duration(std::get(cmd.value)); + update_system_json(sys, "maxDuration", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_MIN_TX_DURATION: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_min_tx_duration(); + sys->set_min_tx_duration(std::get(cmd.value)); + update_system_json(sys, "minTransmissionDuration", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_RECORD_UNKNOWN: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_record_unknown(); + sys->set_record_unknown(std::get(cmd.value)); + update_system_json(sys, "recordUnknown", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_HIDE_ENCRYPTED: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_hideEncrypted(); + sys->set_hideEncrypted(std::get(cmd.value)); + update_system_json(sys, "hideEncrypted", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_HIDE_UNKNOWN: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_hideUnknown(); + sys->set_hideUnknown(std::get(cmd.value)); + update_system_json(sys, "hideUnknownTalkgroups", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_CONVERSATION_MODE: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_conversation_mode(); + sys->set_conversation_mode(std::get(cmd.value)); + update_system_json(sys, "conversationMode", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_TAU: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = static_cast(sys->get_tau()); + sys->set_tau(static_cast(std::get(cmd.value))); + update_system_json(sys, "deemphasisTau", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_MAX_DEV: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_max_dev(); + sys->set_max_dev(std::get(cmd.value)); + update_system_json(sys, "maxDev", cmd.value); + break; + } + case ConfigCommandType::SET_SYSTEM_FILTER_WIDTH: { + System* sys = find_system(cmd.target_id); + if (!sys) return ConfigResult::INVALID_TARGET; + old_value = sys->get_filter_width(); + sys->set_filter_width(std::get(cmd.value)); + update_system_json(sys, "filterWidth", cmd.value); + break; + } + + // Config commands + case ConfigCommandType::SET_CALL_TIMEOUT: { + if (!m_config) return ConfigResult::INTERNAL_ERROR; + old_value = m_config->call_timeout; + m_config->call_timeout = std::get(cmd.value); + update_config_json("callTimeout", cmd.value); + break; + } + } + + return ConfigResult::SUCCESS; +} + +// ============================================================ +// Main loop integration +// ============================================================ + +void ConfigurationService::process_pending_changes() { + std::queue commands_to_process; + + // Quickly move commands to local queue to minimize lock time + { + std::lock_guard lock(m_queue_mutex); + std::swap(commands_to_process, m_pending_commands); + } + + // Process each command + while (!commands_to_process.empty()) { + ConfigCommand cmd = commands_to_process.front(); + commands_to_process.pop(); + + // Validate + ConfigResult result = validate_command(cmd); + std::string message; + + if (result == ConfigResult::SUCCESS) { + // Execute + ConfigValue old_value; + result = execute_command(cmd, old_value); + + if (result == ConfigResult::SUCCESS) { + // Build change info for notification + ConfigChangeInfo change; + change.type = cmd.type; + change.target_id = cmd.target_id; + change.param_name = cmd.param_name; + change.old_value = old_value; + change.new_value = cmd.value; + change.requester = cmd.requester; + + // Notify listeners + notify_listeners(change); + + message = "Change applied successfully"; + } else { + message = "Execution failed: " + result_to_string(result); + } + } else { + message = "Validation failed: " + result_to_string(result); + } + + // Log the change + log_change(cmd, result, message); + + // Invoke callback if provided + if (cmd.callback) { + try { + cmd.callback(result, message); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "ConfigurationService: Callback threw exception: " << e.what(); + } + } + } +} + +size_t ConfigurationService::pending_count() { + std::lock_guard lock(m_queue_mutex); + return m_pending_commands.size(); +} + +// ============================================================ +// Logging helpers +// ============================================================ + +void ConfigurationService::log_change(const ConfigCommand& cmd, ConfigResult result, const std::string& message) { + std::stringstream ss; + ss << "ConfigService: [" << cmd.requester << "] "; + ss << command_type_to_string(cmd.type); + + if (cmd.target_id >= 0) { + ss << " (target=" << cmd.target_id; + if (!cmd.param_name.empty()) { + ss << ", param=" << cmd.param_name; + } + ss << ")"; + } + + ss << " -> " << result_to_string(result); + + if (result == ConfigResult::SUCCESS) { + // Log value for successful changes + if (std::holds_alternative(cmd.value)) { + ss << " [value=" << std::get(cmd.value) << "]"; + } else if (std::holds_alternative(cmd.value)) { + ss << " [value=" << std::get(cmd.value) << "]"; + } else if (std::holds_alternative(cmd.value)) { + ss << " [value=" << (std::get(cmd.value) ? "true" : "false") << "]"; + } else if (std::holds_alternative(cmd.value)) { + ss << " [value=" << std::get(cmd.value) << "]"; + } + BOOST_LOG_TRIVIAL(info) << ss.str(); + } else { + ss << ": " << message; + BOOST_LOG_TRIVIAL(warning) << ss.str(); + } +} + +std::string ConfigurationService::command_type_to_string(ConfigCommandType type) { + switch (type) { + case ConfigCommandType::SET_SOURCE_GAIN: return "SET_SOURCE_GAIN"; + case ConfigCommandType::SET_SOURCE_GAIN_BY_NAME: return "SET_SOURCE_GAIN_BY_NAME"; + case ConfigCommandType::SET_SOURCE_ERROR: return "SET_SOURCE_ERROR"; + case ConfigCommandType::SET_SOURCE_PPM: return "SET_SOURCE_PPM"; + case ConfigCommandType::SET_SOURCE_GAIN_MODE: return "SET_SOURCE_GAIN_MODE"; + case ConfigCommandType::SET_SOURCE_ANTENNA: return "SET_SOURCE_ANTENNA"; + case ConfigCommandType::SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD: return "SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD"; + case ConfigCommandType::SET_SYSTEM_SQUELCH_DB: return "SET_SYSTEM_SQUELCH_DB"; + case ConfigCommandType::SET_SYSTEM_ANALOG_LEVELS: return "SET_SYSTEM_ANALOG_LEVELS"; + case ConfigCommandType::SET_SYSTEM_DIGITAL_LEVELS: return "SET_SYSTEM_DIGITAL_LEVELS"; + case ConfigCommandType::SET_SYSTEM_MIN_DURATION: return "SET_SYSTEM_MIN_DURATION"; + case ConfigCommandType::SET_SYSTEM_MAX_DURATION: return "SET_SYSTEM_MAX_DURATION"; + case ConfigCommandType::SET_SYSTEM_MIN_TX_DURATION: return "SET_SYSTEM_MIN_TX_DURATION"; + case ConfigCommandType::SET_SYSTEM_RECORD_UNKNOWN: return "SET_SYSTEM_RECORD_UNKNOWN"; + case ConfigCommandType::SET_SYSTEM_HIDE_ENCRYPTED: return "SET_SYSTEM_HIDE_ENCRYPTED"; + case ConfigCommandType::SET_SYSTEM_HIDE_UNKNOWN: return "SET_SYSTEM_HIDE_UNKNOWN"; + case ConfigCommandType::SET_SYSTEM_CONVERSATION_MODE: return "SET_SYSTEM_CONVERSATION_MODE"; + case ConfigCommandType::SET_SYSTEM_TAU: return "SET_SYSTEM_TAU"; + case ConfigCommandType::SET_SYSTEM_MAX_DEV: return "SET_SYSTEM_MAX_DEV"; + case ConfigCommandType::SET_SYSTEM_FILTER_WIDTH: return "SET_SYSTEM_FILTER_WIDTH"; + case ConfigCommandType::SET_CALL_TIMEOUT: return "SET_CALL_TIMEOUT"; + default: return "UNKNOWN"; + } +} + +std::string ConfigurationService::result_to_string(ConfigResult result) { + switch (result) { + case ConfigResult::SUCCESS: return "SUCCESS"; + case ConfigResult::INVALID_TARGET: return "INVALID_TARGET"; + case ConfigResult::INVALID_VALUE: return "INVALID_VALUE"; + case ConfigResult::INVALID_PARAM_NAME: return "INVALID_PARAM_NAME"; + case ConfigResult::TYPE_MISMATCH: return "TYPE_MISMATCH"; + case ConfigResult::INTERNAL_ERROR: return "INTERNAL_ERROR"; + default: return "UNKNOWN"; + } +} + diff --git a/trunk-recorder/config_service.h b/trunk-recorder/config_service.h new file mode 100644 index 00000000..9666d88f --- /dev/null +++ b/trunk-recorder/config_service.h @@ -0,0 +1,232 @@ +#ifndef CONFIG_SERVICE_H +#define CONFIG_SERVICE_H + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "global_structs.h" + +// Forward declarations to avoid circular includes +class Source; +class System; + +// Value types that can be set via the configuration service +using ConfigValue = std::variant; + +// Command types for all modifiable parameters +enum class ConfigCommandType { + // Source parameters + SET_SOURCE_GAIN, + SET_SOURCE_GAIN_BY_NAME, + SET_SOURCE_ERROR, + SET_SOURCE_PPM, + SET_SOURCE_GAIN_MODE, + SET_SOURCE_ANTENNA, + SET_SOURCE_SIGNAL_DETECTOR_THRESHOLD, + + // System parameters + SET_SYSTEM_SQUELCH_DB, + SET_SYSTEM_ANALOG_LEVELS, + SET_SYSTEM_DIGITAL_LEVELS, + SET_SYSTEM_MIN_DURATION, + SET_SYSTEM_MAX_DURATION, + SET_SYSTEM_MIN_TX_DURATION, + SET_SYSTEM_RECORD_UNKNOWN, + SET_SYSTEM_HIDE_ENCRYPTED, + SET_SYSTEM_HIDE_UNKNOWN, + SET_SYSTEM_CONVERSATION_MODE, + SET_SYSTEM_TAU, + SET_SYSTEM_MAX_DEV, + SET_SYSTEM_FILTER_WIDTH, + + // Config parameters + SET_CALL_TIMEOUT +}; + +// Result of a configuration change +enum class ConfigResult { + SUCCESS, + INVALID_TARGET, // Source/System not found + INVALID_VALUE, // Value out of range or wrong type + INVALID_PARAM_NAME, // For gain_by_name with unknown name + TYPE_MISMATCH, // Wrong variant type for command + INTERNAL_ERROR // Unexpected error +}; + +// Callback type for async notification +using ConfigCallback = std::function; + +// Configuration change command +struct ConfigCommand { + ConfigCommandType type; + int target_id; // source_num or sys_num (-1 for global config) + std::string param_name; // Used for SET_SOURCE_GAIN_BY_NAME + ConfigValue value; + ConfigCallback callback; // Optional callback for result notification + std::string requester; // Name of plugin/entity requesting change + + ConfigCommand() : type(ConfigCommandType::SET_SOURCE_GAIN), target_id(-1), callback(nullptr) {} +}; + +// Information about a configuration change (for notifications) +struct ConfigChangeInfo { + ConfigCommandType type; + int target_id; + std::string param_name; + ConfigValue old_value; + ConfigValue new_value; + std::string requester; +}; + +// Listener callback type for change notifications +using ConfigChangeListener = std::function; + +class ConfigurationService { +public: + ConfigurationService(); + ~ConfigurationService(); + + // Initialize the service with references to sources, systems, and config + void init(Config* config, std::vector* sources, std::vector* systems); + + // ============================================================ + // Command submission (thread-safe, called by plugins) + // ============================================================ + + // Submit a configuration change command + // Returns immediately; actual change happens in process_pending_changes() + bool submit_command(const ConfigCommand& cmd); + + // Convenience methods for common operations + bool set_source_gain(int source_num, double gain, const std::string& requester, ConfigCallback callback = nullptr); + bool set_source_gain_by_name(int source_num, const std::string& name, double gain, const std::string& requester, ConfigCallback callback = nullptr); + bool set_source_error(int source_num, double error, const std::string& requester, ConfigCallback callback = nullptr); + bool set_source_ppm(int source_num, double ppm, const std::string& requester, ConfigCallback callback = nullptr); + bool set_source_gain_mode(int source_num, bool agc, const std::string& requester, ConfigCallback callback = nullptr); + bool set_source_antenna(int source_num, const std::string& antenna, const std::string& requester, ConfigCallback callback = nullptr); + bool set_source_signal_detector_threshold(int source_num, double threshold, const std::string& requester, ConfigCallback callback = nullptr); + + bool set_system_squelch(int sys_num, double squelch_db, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_analog_levels(int sys_num, double levels, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_digital_levels(int sys_num, double levels, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_min_duration(int sys_num, double duration, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_max_duration(int sys_num, double duration, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_min_tx_duration(int sys_num, double duration, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_record_unknown(int sys_num, bool record, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_hide_encrypted(int sys_num, bool hide, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_hide_unknown(int sys_num, bool hide, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_conversation_mode(int sys_num, bool mode, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_tau(int sys_num, float tau, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_max_dev(int sys_num, int max_dev, const std::string& requester, ConfigCallback callback = nullptr); + bool set_system_filter_width(int sys_num, double width, const std::string& requester, ConfigCallback callback = nullptr); + + bool set_call_timeout(double timeout, const std::string& requester, ConfigCallback callback = nullptr); + + // Save the current configuration to disk (only writes keys that were + // in the original config file or explicitly modified at runtime) + bool save_config(); + + // ============================================================ + // Getters (thread-safe snapshots) + // ============================================================ + + std::optional get_source_gain(int source_num); + std::optional get_source_error(int source_num); + std::optional get_source_gain_mode(int source_num); + std::optional get_source_antenna(int source_num); + + std::optional get_system_squelch(int sys_num); + std::optional get_system_analog_levels(int sys_num); + std::optional get_system_digital_levels(int sys_num); + std::optional get_system_min_duration(int sys_num); + std::optional get_system_max_duration(int sys_num); + std::optional get_system_record_unknown(int sys_num); + std::optional get_system_conversation_mode(int sys_num); + + double get_call_timeout(); + + // Get source/system by ID + Source* get_source(int source_num); + System* get_system(int sys_num); + System* get_system_by_short_name(const std::string& short_name); + + // ============================================================ + // Change notification + // ============================================================ + + // Register a listener to be notified of all configuration changes + void register_change_listener(ConfigChangeListener listener); + + // ============================================================ + // Main loop integration + // ============================================================ + + // Process all pending configuration changes + // Called from the main loop (single-threaded context) + void process_pending_changes(); + + // Get the number of pending changes + size_t pending_count(); + +private: + // Find source by number + Source* find_source(int source_num); + + // Find system by number + System* find_system(int sys_num); + + // Validate command before execution + ConfigResult validate_command(const ConfigCommand& cmd); + + // Execute a single command + ConfigResult execute_command(const ConfigCommand& cmd, ConfigValue& old_value); + + // Notify all listeners of a change + void notify_listeners(const ConfigChangeInfo& change); + + // Log a configuration change + void log_change(const ConfigCommand& cmd, ConfigResult result, const std::string& message); + + // Get string representation of command type + static std::string command_type_to_string(ConfigCommandType type); + + // Get string representation of result + static std::string result_to_string(ConfigResult result); + + // Update loaded_json for source/system/global config changes + void update_source_json(Source* src, const std::string& json_key, const ConfigValue& value); + void update_system_json(System* sys, const std::string& json_key, const ConfigValue& value); + void update_config_json(const std::string& json_key, const ConfigValue& value); + + // Members + Config* m_config; + std::vector* m_sources; + std::vector* m_systems; + + std::mutex m_queue_mutex; + std::queue m_pending_commands; + + std::mutex m_listeners_mutex; + std::vector m_change_listeners; + + bool m_initialized; +}; + +// Global configuration service instance +extern ConfigurationService* g_config_service; + +// Initialize the global configuration service +void init_config_service(Config* config, std::vector* sources, std::vector* systems); + +// Shutdown the global configuration service +void shutdown_config_service(); + +#endif // CONFIG_SERVICE_H + diff --git a/trunk-recorder/examples/example_config_plugin.cc b/trunk-recorder/examples/example_config_plugin.cc new file mode 100644 index 00000000..3663909a --- /dev/null +++ b/trunk-recorder/examples/example_config_plugin.cc @@ -0,0 +1,236 @@ +/** + * Example Plugin: Dynamic Configuration + * + * See example_config_plugin.h for documentation. + */ + +#include "example_config_plugin.h" +#include + +// Factory function +boost::shared_ptr create_plugin() { + return boost::make_shared(); +} + +ExampleConfigPlugin::ExampleConfigPlugin() + : m_auto_gain_enabled(false), + m_target_signal_level(-30.0), + m_gain_adjustment_step(1.0), + m_poll_interval_ms(5000), + m_config(nullptr), + m_avg_signal_level(0.0), + m_signal_sample_count(0) { + m_last_adjustment_time = std::chrono::steady_clock::now(); +} + +int ExampleConfigPlugin::parse_config(json config_data) { + // Parse plugin-specific configuration + // Example config.json section: + // { + // "name": "example_config_plugin", + // "library": "libexample_config_plugin.so", + // "autoGainEnabled": true, + // "targetSignalLevel": -25.0, + // "gainAdjustmentStep": 0.5, + // "pollIntervalMs": 10000 + // } + + m_auto_gain_enabled = config_data.value("autoGainEnabled", false); + m_target_signal_level = config_data.value("targetSignalLevel", -30.0); + m_gain_adjustment_step = config_data.value("gainAdjustmentStep", 1.0); + m_poll_interval_ms = config_data.value("pollIntervalMs", 5000); + + BOOST_LOG_TRIVIAL(info) << "ExampleConfigPlugin: Auto-gain " + << (m_auto_gain_enabled ? "enabled" : "disabled") + << ", target signal level: " << m_target_signal_level << " dB"; + + return 0; +} + +int ExampleConfigPlugin::init(Config *config, std::vector sources, std::vector systems) { + m_config = config; + m_sources = sources; + m_systems = systems; + + BOOST_LOG_TRIVIAL(info) << "ExampleConfigPlugin: Initialized with " + << sources.size() << " sources and " + << systems.size() << " systems"; + + // Log initial configuration values + log_current_config(); + + return 0; +} + +int ExampleConfigPlugin::start() { + BOOST_LOG_TRIVIAL(info) << "ExampleConfigPlugin: Started"; + + // Example: Register for configuration change notifications + // (This is already done automatically via the plugin manager) + + return 0; +} + +int ExampleConfigPlugin::stop() { + BOOST_LOG_TRIVIAL(info) << "ExampleConfigPlugin: Stopped"; + return 0; +} + +int ExampleConfigPlugin::poll_one() { + // Check if enough time has passed since last adjustment + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - m_last_adjustment_time); + + if (elapsed.count() < m_poll_interval_ms) { + return 0; + } + + m_last_adjustment_time = now; + + // Example: Periodic gain adjustment based on average signal level + if (m_auto_gain_enabled && m_signal_sample_count > 0) { + for (size_t i = 0; i < m_sources.size(); i++) { + adjust_gain_for_source(static_cast(i)); + } + + // Reset signal tracking + m_avg_signal_level = 0.0; + m_signal_sample_count = 0; + } + + return 0; +} + +int ExampleConfigPlugin::on_config_change(const ConfigChangeInfo& change) { + // React to configuration changes made by other plugins or external sources + + std::string change_type; + switch (change.type) { + case ConfigCommandType::SET_SOURCE_GAIN: + change_type = "Source Gain"; + break; + case ConfigCommandType::SET_SOURCE_ERROR: + change_type = "Source Error"; + break; + case ConfigCommandType::SET_SYSTEM_SQUELCH_DB: + change_type = "System Squelch"; + break; + case ConfigCommandType::SET_CALL_TIMEOUT: + change_type = "Call Timeout"; + break; + default: + change_type = "Other"; + break; + } + + BOOST_LOG_TRIVIAL(debug) << "ExampleConfigPlugin: Configuration changed - " + << change_type << " (target=" << change.target_id << ")" + << " by " << change.requester; + + // Example: If squelch is changed, we might want to adjust gain accordingly + if (change.type == ConfigCommandType::SET_SYSTEM_SQUELCH_DB) { + // Plugin could react to squelch changes here + } + + return 0; +} + +int ExampleConfigPlugin::call_end(Call_Data_t call_info) { + // Track signal quality for auto-gain adjustment + if (m_auto_gain_enabled && call_info.signal != 0) { + // Update running average + m_avg_signal_level = ((m_avg_signal_level * m_signal_sample_count) + call_info.signal) + / (m_signal_sample_count + 1); + m_signal_sample_count++; + + BOOST_LOG_TRIVIAL(trace) << "ExampleConfigPlugin: Signal sample " + << call_info.signal << " dB, avg: " + << m_avg_signal_level << " dB"; + } + + return 0; +} + +void ExampleConfigPlugin::adjust_gain_for_source(int source_num) { + ConfigurationService* config_service = get_config_service(); + if (!config_service) { + BOOST_LOG_TRIVIAL(warning) << "ExampleConfigPlugin: ConfigurationService not available"; + return; + } + + // Get current gain + auto current_gain = config_service->get_source_gain(source_num); + if (!current_gain.has_value()) { + return; + } + + double gain = current_gain.value(); + double signal_diff = m_avg_signal_level - m_target_signal_level; + + // Adjust gain based on signal difference + // If signal is too weak (below target), increase gain + // If signal is too strong (above target), decrease gain + double new_gain = gain; + + if (signal_diff < -3.0) { + // Signal too weak, increase gain + new_gain = gain + m_gain_adjustment_step; + } else if (signal_diff > 3.0) { + // Signal too strong, decrease gain + new_gain = gain - m_gain_adjustment_step; + } else { + // Signal within acceptable range + return; + } + + // Clamp gain to valid range + new_gain = std::max(0.0, std::min(100.0, new_gain)); + + if (new_gain == gain) { + return; // No change needed + } + + BOOST_LOG_TRIVIAL(info) << "ExampleConfigPlugin: Adjusting source " << source_num + << " gain from " << gain << " to " << new_gain + << " (avg signal: " << m_avg_signal_level << " dB)"; + + // Submit the gain change request + config_service->set_source_gain(source_num, new_gain, "ExampleConfigPlugin", + [source_num](ConfigResult result, const std::string& message) { + if (result == ConfigResult::SUCCESS) { + BOOST_LOG_TRIVIAL(debug) << "ExampleConfigPlugin: Gain change for source " + << source_num << " applied successfully"; + } else { + BOOST_LOG_TRIVIAL(warning) << "ExampleConfigPlugin: Gain change failed: " << message; + } + }); +} + +void ExampleConfigPlugin::log_current_config() { + ConfigurationService* config_service = get_config_service(); + if (!config_service) { + return; + } + + BOOST_LOG_TRIVIAL(info) << "ExampleConfigPlugin: Current configuration snapshot:"; + BOOST_LOG_TRIVIAL(info) << " Call timeout: " << config_service->get_call_timeout() << " seconds"; + + for (size_t i = 0; i < m_sources.size(); i++) { + auto gain = config_service->get_source_gain(static_cast(i)); + auto error = config_service->get_source_error(static_cast(i)); + + BOOST_LOG_TRIVIAL(info) << " Source " << i << ": gain=" + << (gain.has_value() ? std::to_string(gain.value()) : "N/A") + << ", error=" + << (error.has_value() ? std::to_string(error.value()) : "N/A"); + } + + for (size_t i = 0; i < m_systems.size(); i++) { + System* sys = m_systems[i]; + auto squelch = config_service->get_system_squelch(sys->get_sys_num()); + + BOOST_LOG_TRIVIAL(info) << " System " << sys->get_short_name() << ": squelch=" + << (squelch.has_value() ? std::to_string(squelch.value()) : "N/A") << " dB"; + } +} + diff --git a/trunk-recorder/examples/example_config_plugin.h b/trunk-recorder/examples/example_config_plugin.h new file mode 100644 index 00000000..b613c400 --- /dev/null +++ b/trunk-recorder/examples/example_config_plugin.h @@ -0,0 +1,67 @@ +/** + * Example Plugin: Dynamic Configuration + * + * This example plugin demonstrates how to use the ConfigurationService + * to dynamically modify Trunk Recorder's configuration at runtime. + * + * Features demonstrated: + * - Adjusting source gain based on signal quality + * - Modifying system squelch levels + * - Reacting to configuration changes made by other plugins + * - Using callbacks for async notification of change results + */ + +#ifndef EXAMPLE_CONFIG_PLUGIN_H +#define EXAMPLE_CONFIG_PLUGIN_H + +#include "../plugin_manager/plugin_api.h" +#include + +class ExampleConfigPlugin : public Plugin_Api { +public: + ExampleConfigPlugin(); + + // Plugin lifecycle + int parse_config(json config_data) override; + int init(Config *config, std::vector sources, std::vector systems) override; + int start() override; + int stop() override; + + // Called every poll cycle - use for periodic adjustments + int poll_one() override; + + // Called when any configuration parameter changes + int on_config_change(const ConfigChangeInfo& change) override; + + // Optional: React to call events for signal-based gain adjustment + int call_end(Call_Data_t call_info) override; + +private: + // Configuration from plugin config + bool m_auto_gain_enabled; + double m_target_signal_level; + double m_gain_adjustment_step; + int m_poll_interval_ms; + + // State + std::vector m_sources; + std::vector m_systems; + Config* m_config; + std::chrono::steady_clock::time_point m_last_adjustment_time; + + // Track signal quality for auto-gain + double m_avg_signal_level; + int m_signal_sample_count; + + // Helper methods + void adjust_gain_for_source(int source_num); + void log_current_config(); +}; + +// Factory function for plugin loading +extern "C" { + boost::shared_ptr create_plugin(); +} + +#endif // EXAMPLE_CONFIG_PLUGIN_H + diff --git a/trunk-recorder/global_structs.h b/trunk-recorder/global_structs.h index 5210d357..62e1d5fc 100644 --- a/trunk-recorder/global_structs.h +++ b/trunk-recorder/global_structs.h @@ -26,6 +26,7 @@ struct Transmission { }; struct Config { + nlohmann::json loaded_json; // Raw JSON from config file - tracks which keys were explicitly set std::string config_file; std::string upload_script; std::string upload_server; diff --git a/trunk-recorder/main.cc b/trunk-recorder/main.cc index 3e90ba65..e3fa9b38 100644 --- a/trunk-recorder/main.cc +++ b/trunk-recorder/main.cc @@ -66,6 +66,7 @@ #include #include "plugin_manager/plugin_manager.h" +#include "config_service.h" #include "cmake.h" #include "git.h" @@ -121,7 +122,13 @@ int main(int argc, char **argv) { exit(1); } + // Initialize the configuration service for runtime config changes + init_config_service(&config, &sources, &systems); + start_plugins(sources, systems); + + // Register plugin manager to receive configuration change notifications + plugman_register_config_listener(); if (setup_systems(config, tb, sources, systems, calls)) { @@ -138,6 +145,9 @@ int main(int argc, char **argv) { BOOST_LOG_TRIVIAL(info) << "stopping plugins" << std::endl; stop_plugins(); + + BOOST_LOG_TRIVIAL(info) << "shutting down configuration service" << std::endl; + shutdown_config_service(); } else { BOOST_LOG_TRIVIAL(error) << "Unable to setup a System to record, exiting..." << std::endl; } diff --git a/trunk-recorder/monitor_systems.cc b/trunk-recorder/monitor_systems.cc index dcf48d0e..ab9ac6dd 100644 --- a/trunk-recorder/monitor_systems.cc +++ b/trunk-recorder/monitor_systems.cc @@ -900,6 +900,11 @@ int monitor_messages(Config &config, gr::top_block_sptr &tb, std::vectorprocess_pending_changes(); + } for (vector::iterator sys_it = systems.begin(); sys_it != systems.end(); sys_it++) { System_impl *system = (System_impl *)*sys_it; diff --git a/trunk-recorder/monitor_systems.h b/trunk-recorder/monitor_systems.h index d4dee670..e74cf6e2 100644 --- a/trunk-recorder/monitor_systems.h +++ b/trunk-recorder/monitor_systems.h @@ -6,6 +6,7 @@ #include "./global_structs.h" #include "call.h" #include "config.h" +#include "config_service.h" #include "source.h" #include "systems/p25_parser.h" #include "systems/p25_trunking.h" diff --git a/trunk-recorder/plugin_manager/plugin_api.h b/trunk-recorder/plugin_manager/plugin_api.h index c67a205d..418b475e 100644 --- a/trunk-recorder/plugin_manager/plugin_api.h +++ b/trunk-recorder/plugin_manager/plugin_api.h @@ -7,6 +7,7 @@ #include "../systems/system.h" #include "../systems/parser.h" #include "../formatter.h" +#include "../config_service.h" #include @@ -47,6 +48,14 @@ class Plugin_Api { virtual int unit_data_grant(System *sys, long source_id) { return 0; }; virtual int unit_answer_request(System *sys, long source_id, long talkgroup) { return 0; }; virtual int unit_location(System *sys, long source_id, long talkgroup_num) { return 0; }; + + // Called when a configuration parameter is changed via the ConfigurationService + // Allows plugins to react to configuration changes made by other plugins or external sources + virtual int on_config_change(const ConfigChangeInfo& change) { return 0; }; + + // Get a pointer to the global configuration service (for making changes) + ConfigurationService* get_config_service() { return g_config_service; } + //void set_frequency_format(int f) { frequencyFormat = f; } virtual ~Plugin_Api(){}; }; diff --git a/trunk-recorder/plugin_manager/plugin_manager.cc b/trunk-recorder/plugin_manager/plugin_manager.cc index 6e6a292f..f4b7ff36 100644 --- a/trunk-recorder/plugin_manager/plugin_manager.cc +++ b/trunk-recorder/plugin_manager/plugin_manager.cc @@ -344,3 +344,21 @@ void plugman_unit_location(System *system, long source_id, long talkgroup_num) { } } } + +void plugman_config_change(const ConfigChangeInfo& change) { + for (std::vector::iterator it = plugins.begin(); it != plugins.end(); it++) { + Plugin *plugin = *it; + if (plugin->state == PLUGIN_RUNNING) { + plugin->api->on_config_change(change); + } + } +} + +void plugman_register_config_listener() { + if (g_config_service != nullptr) { + g_config_service->register_change_listener([](const ConfigChangeInfo& change) { + plugman_config_change(change); + }); + BOOST_LOG_TRIVIAL(info) << "Plugin Manager registered as configuration change listener"; + } +} diff --git a/trunk-recorder/plugin_manager/plugin_manager.h b/trunk-recorder/plugin_manager/plugin_manager.h index 7d77d237..2e59551d 100644 --- a/trunk-recorder/plugin_manager/plugin_manager.h +++ b/trunk-recorder/plugin_manager/plugin_manager.h @@ -2,6 +2,7 @@ #define PLUGIN_MANAGER_H #include "../call_concluder/call_concluder.h" +#include "../config_service.h" #include "../recorders/recorder.h" #include "../source.h" #include "../systems/system.h" @@ -50,4 +51,9 @@ void plugman_unit_group_affiliation(System *system, long source_id, long talkgro void plugman_unit_data_grant(System *system, long source_id); void plugman_unit_answer_request(System *system, long source_id, long talkgroup); void plugman_unit_location(System *system, long source_id, long talkgroup_num); +void plugman_config_change(const ConfigChangeInfo& change); + +// Register the plugin manager as a listener for configuration changes +void plugman_register_config_listener(); + #endif // PLUGIN_MANAGER_H diff --git a/trunk-recorder/source.cc b/trunk-recorder/source.cc index db2b5106..cfef36cc 100644 --- a/trunk-recorder/source.cc +++ b/trunk-recorder/source.cc @@ -9,6 +9,14 @@ int Source::get_num() { return src_num; }; +int Source::get_config_index() { + return config_index; +} + +void Source::set_config_index(int index) { + config_index = index; +} + gr::basic_block_sptr Source::get_src_block() { return source_block; } @@ -62,6 +70,7 @@ Source::Source(double c, double r, double e, std::string drv, std::string dev, C max_sigmf_recorders = 0; max_analog_recorders = 0; debug_recorder_port = 0; + config_index = -1; attached_detector = false; attached_selector = false; next_selector_port = 0; @@ -168,6 +177,7 @@ void Source::set_iq_source(std::string iq_file, bool repeat, double center, doub max_sigmf_recorders = 0; max_analog_recorders = 0; debug_recorder_port = 0; + config_index = -1; attached_detector = false; attached_selector = false; next_selector_port = 0; @@ -306,6 +316,12 @@ void Source::set_freq_corr(double p) { void Source::set_error(double e) { error = e; + if (driver == "osmosdr") { + cast_to_osmo_sptr(source_block)->set_center_freq(center + error, 0); + } else if (driver == "usrp") { + cast_to_usrp_sptr(source_block)->set_center_freq(center + error, 0); + } + } double Source::get_error() { @@ -371,6 +387,11 @@ double Source::get_gain() { return gain; } + +bool Source::get_gain_mode() { + return gain_mode; +} + void Source::set_gain_mode(bool m) { if (driver == "osmosdr") { gain_mode = m; diff --git a/trunk-recorder/source.h b/trunk-recorder/source.h index 7fd43e7f..85858536 100644 --- a/trunk-recorder/source.h +++ b/trunk-recorder/source.h @@ -53,6 +53,7 @@ class Source { int debug_recorder_port; int next_selector_port; int silence_frames; + int config_index; Config *config; bool autotune_source; @@ -76,6 +77,8 @@ class Source { public: int get_num(); + int get_config_index(); + void set_config_index(int index); Config *get_config(); Source(double c, double r, double e, std::string driver, std::string device, Config *cfg); Source(std::string sigmf_meta, std::string sigmf_data, bool repeat, Config *cfg); diff --git a/trunk-recorder/systems/system.h b/trunk-recorder/systems/system.h index 6ec7fe2f..4f20d469 100644 --- a/trunk-recorder/systems/system.h +++ b/trunk-recorder/systems/system.h @@ -202,5 +202,8 @@ class System { virtual std::string get_filename_format() = 0; virtual void set_filename_format(std::string format) = 0; + virtual int get_config_index() = 0; + virtual void set_config_index(int index) = 0; + }; #endif diff --git a/trunk-recorder/systems/system_impl.cc b/trunk-recorder/systems/system_impl.cc index 1bea7417..d4343174 100644 --- a/trunk-recorder/systems/system_impl.cc +++ b/trunk-recorder/systems/system_impl.cc @@ -102,6 +102,7 @@ System_impl::System_impl(int sys_num) { retune_attempts = 0; message_count = 0; decode_rate = 0; + d_config_index = -1; msg_queue = gr::msg_queue::make(100); } @@ -790,4 +791,12 @@ std::string System_impl::get_filename_format() { void System_impl::set_filename_format(std::string format) { this->filename_format = format; +} + +int System_impl::get_config_index() { + return d_config_index; +} + +void System_impl::set_config_index(int index) { + d_config_index = index; } \ No newline at end of file diff --git a/trunk-recorder/systems/system_impl.h b/trunk-recorder/systems/system_impl.h index eac43648..f571ae53 100644 --- a/trunk-recorder/systems/system_impl.h +++ b/trunk-recorder/systems/system_impl.h @@ -269,6 +269,9 @@ class System_impl : public System { std::string get_filename_format() override; void set_filename_format(std::string format) override; + int get_config_index() override; + void set_config_index(int index) override; + private: TalkgroupDisplayFormat talkgroup_display_format; bool d_hideEncrypted; @@ -282,5 +285,6 @@ class System_impl : public System { bool d_fsync_enabled; bool d_star_enabled; bool d_tps_enabled; + int d_config_index; }; #endif