#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 + "'"); } } 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); // ---- 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