fwt_software/src/core/Config.cpp

110 lines
4.3 KiB
C++

#include "fgc/Config.h"
#include "fgc/Paths.h"
#include <cstdlib>
#include <stdexcept>
extern "C" {
#include "ini.h"
}
namespace fgc {
namespace {
std::string get(const std::map<std::string, std::string>& 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<std::string, std::string>& 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<std::string, std::string>& 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<std::map<std::string, std::string>*>(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<std::string, std::string>& 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<unsigned>(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<std::string, std::string> 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