fwt_software/src/core/Config.cpp

155 lines
6.7 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 + "'");
}
}
double getDouble(const std::map<std::string, std::string>& 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<std::string, std::string>& 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<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);
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<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