#include "fgc/Config.h" #include "fgc/Paths.h" #include #include extern "C" { #include "ini.h" } namespace fgc { namespace { std::string get(const std::map& kv, const std::string& key, const std::string& fallback = "") { auto it = kv.find(key); return (it != kv.end() && !it->second.empty()) ? it->second : fallback; } int getInt(const std::map& kv, const std::string& key, int fallback) { auto it = kv.find(key); if (it == kv.end() || it->second.empty()) return fallback; try { return std::stoi(it->second); } catch (const std::exception&) { throw std::runtime_error("Config key '" + key + "' must be an integer, got '" + it->second + "'"); } } double getDouble(const std::map& kv, const std::string& key, double fallback) { auto it = kv.find(key); if (it == kv.end() || it->second.empty()) return fallback; try { return std::stod(it->second); } catch (const std::exception&) { throw std::runtime_error("Config key '" + key + "' must be a number, got '" + it->second + "'"); } } long getLong(const std::map& kv, const std::string& key, long fallback) { auto it = kv.find(key); if (it == kv.end() || it->second.empty()) return fallback; try { return std::stol(it->second); } catch (const std::exception&) { throw std::runtime_error("Config key '" + key + "' must be an integer, got '" + it->second + "'"); } } bool getBool(const std::map& kv, const std::string& key, bool fallback) { auto it = kv.find(key); if (it == kv.end() || it->second.empty()) return fallback; const std::string& v = it->second; if (v == "1" || v == "true" || v == "yes" || v == "on") return true; if (v == "0" || v == "false" || v == "no" || v == "off") return false; throw std::runtime_error("Config key '" + key + "' must be a boolean, got '" + v + "'"); } std::string envOr(const char* name, const std::string& fallback) { const char* v = std::getenv(name); return (v && *v) ? std::string(v) : fallback; } // inih callback: flatten "Section.name" => value into the map. int iniHandler(void* user, const char* section, const char* name, const char* value) { auto* kv = static_cast*>(user); (*kv)[std::string(section) + "." + std::string(name)] = value ? value : ""; return 1; } } // namespace double AppConfig::image_rate() const { return general.image_interval > 0 ? 1.0 / general.image_interval : 0.0; } AppConfig ConfigLoader::fromMap(const std::map& kv) { AppConfig cfg; cfg.general.tower_name = get(kv, "General.tower_name", cfg.general.tower_name); cfg.general.image_interval = getInt(kv, "General.image_interval", cfg.general.image_interval); cfg.general.debug = getBool(kv, "General.debug", cfg.general.debug); cfg.network.broker_ip = get(kv, "Network.zkms_server_ip", cfg.network.broker_ip); // Secrets: environment variables win over the file. cfg.network.mqtt_user = envOr("FGC_MQTT_USER", get(kv, "Network.mqtt_user")); cfg.network.mqtt_pw = envOr("FGC_MQTT_PW", get(kv, "Network.mqtt_pw")); cfg.serial.device = get(kv, "Serial.device", cfg.serial.device); cfg.serial.baud = static_cast(getInt(kv, "Serial.baud", cfg.serial.baud)); cfg.camera.ids.clear(); for (const char* key : {"Camera.id_Cam1", "Camera.id_Cam2", "Camera.id_Cam3", "Camera.id_Cam4"}) { std::string id = get(kv, key); if (!id.empty()) cfg.camera.ids.push_back(id); } std::string out = get(kv, "Paths.output_dir"); cfg.paths.output_dir = out.empty() ? paths::defaultOutputDir() : paths::expandUser(out); cfg.features.enable_mqtt = getBool(kv, "Features.enable_mqtt", cfg.features.enable_mqtt); cfg.features.enable_camera = getBool(kv, "Features.enable_camera", cfg.features.enable_camera); cfg.features.enable_serial = getBool(kv, "Features.enable_serial", cfg.features.enable_serial); cfg.features.mock_camera = getBool(kv, "Features.mock_camera", cfg.features.mock_camera); cfg.features.mock_serial = getBool(kv, "Features.mock_serial", cfg.features.mock_serial); cfg.logging.level = get(kv, "Logging.level", cfg.logging.level); cfg.logging.trace = get(kv, "Logging.trace", cfg.logging.trace); cfg.ui.enable_tui = getBool(kv, "UI.enable_tui", cfg.ui.enable_tui); // [Motor]: operator-calibrated degrees<->counts maps (see Geometry). cfg.geometry.yaw.counts_per_deg = getDouble(kv, "Motor.yaw_counts_per_deg", cfg.geometry.yaw.counts_per_deg); cfg.geometry.yaw.zero_count = getLong(kv, "Motor.yaw_zero_count", cfg.geometry.yaw.zero_count); cfg.geometry.yaw.min_deg = getDouble(kv, "Motor.yaw_min_deg", cfg.geometry.yaw.min_deg); cfg.geometry.yaw.max_deg = getDouble(kv, "Motor.yaw_max_deg", cfg.geometry.yaw.max_deg); cfg.geometry.pitch.counts_per_deg = getDouble(kv, "Motor.pitch_counts_per_deg", cfg.geometry.pitch.counts_per_deg); cfg.geometry.pitch.zero_count = getLong(kv, "Motor.pitch_zero_count", cfg.geometry.pitch.zero_count); cfg.geometry.pitch.min_deg = getDouble(kv, "Motor.pitch_min_deg", cfg.geometry.pitch.min_deg); cfg.geometry.pitch.max_deg = getDouble(kv, "Motor.pitch_max_deg", cfg.geometry.pitch.max_deg); // [Scan]: scan-grid source (explicit CSV file, else generated). cfg.scan.grid_file = get(kv, "Scan.grid_file", cfg.scan.grid_file); cfg.scan.yaw_intervals = getInt(kv, "Scan.yaw_intervals", cfg.scan.yaw_intervals); cfg.scan.yaw_min_deg = getDouble(kv, "Scan.yaw_min_deg", cfg.scan.yaw_min_deg); cfg.scan.yaw_max_deg = getDouble(kv, "Scan.yaw_max_deg", cfg.scan.yaw_max_deg); cfg.scan.pitch_levels = get(kv, "Scan.pitch_levels", cfg.scan.pitch_levels); // ---- validation ---- if (cfg.general.image_interval <= 0) throw std::runtime_error("General.image_interval must be > 0 (got " + std::to_string(cfg.general.image_interval) + ")"); if (cfg.serial.baud == 0) throw std::runtime_error("Serial.baud must be > 0"); return cfg; } AppConfig ConfigLoader::loadFromFile(const std::string& path) { std::map kv; int rc = ini_parse(path.c_str(), iniHandler, &kv); if (rc < 0) throw std::runtime_error("Cannot read config file: " + path); if (rc > 0) throw std::runtime_error("Parse error in config file " + path + " at line " + std::to_string(rc)); return fromMap(kv); } } // namespace fgc