#pragma once #include "fgc/Geometry.h" #include #include #include #include namespace fgc { // Typed application configuration. Replaces the ad-hoc std::map // lookups that were scattered through main.cpp. struct GeneralConfig { std::string tower_name = "Unnamed"; int image_interval = 5; // seconds between captures bool debug = false; // print motor telemetry each loop tick }; struct NetworkConfig { std::string broker_ip = "127.0.0.1"; // MQTT broker / ZKMS server std::string mqtt_user; // see secrets note below std::string mqtt_pw; }; struct SerialConfig { std::string device = "/dev/ttyACM0"; unsigned int baud = 115200; }; struct CameraConfig { std::vector ids; // GigE IP or USB DEV_ id, in order std::vector labels = {"RGB", "ACR", "NIR"}; // index -> output subfolder }; struct PathsConfig { // Where captured .jxl images are written. Supports leading ~ and $ENV // expansion. Empty => resolved to a sensible default at load time. std::string output_dir; }; struct FeaturesConfig { bool enable_mqtt = true; bool enable_camera = true; bool enable_serial = true; bool enable_imu = false; // Xsens MTi orientation/IMU (off by default) bool mock_camera = false; // use a simulated camera instead of Vimba X bool mock_serial = false; // use a simulated motor controller bool mock_imu = false; // use a simulated IMU instead of the MTi }; // [IMU]: Xsens MTi connected over the LattePanda's RS-232 UART (a hardware // /dev/ttyS* node, stable across reboots — not a USB device). struct ImuConfig { std::string device = ""; // e.g. /dev/ttyS4; empty => required when enabled unsigned int baud = 115200; }; struct LoggingConfig { std::string level; // trace|debug|info|warn|error|off; empty => default (CLI overrides) std::string trace; // verbatim wire-trace categories: serial,mqtt,camera,control,all,none }; struct UiConfig { bool enable_tui = false; // false => headless line console (default); CLI --tui/--no-tui override }; // [Scan]: source of the capture scan grid (the (yaw,pitch) waypoints the // auto-sweep steps through). If grid_file is set, the CSV is loaded verbatim; // otherwise a grid is generated from the parameters below (see ScanGrid). struct ScanConfig { std::string grid_file; // path to an editable yaw_deg,pitch_deg CSV; empty => generate int yaw_intervals = 56; // generated: yaw positions across [yaw_min_deg, yaw_max_deg] double yaw_min_deg = -90.0; double yaw_max_deg = 90.0; std::string pitch_levels = "0"; // generated: comma list of pitch elevations (deg) }; // Which homed endstop becomes yaw 0 deg, with degrees rising toward the other // limit (which then lands near +360, a bit less for the soft-limit/mechanical // gap). Off = use the configured/calibrated yaw zero_count as-is. enum class YawHomeZero { Off, Low, High }; struct AppConfig { GeneralConfig general; NetworkConfig network; SerialConfig serial; CameraConfig camera; PathsConfig paths; FeaturesConfig features; LoggingConfig logging; UiConfig ui; // [UI] terminal dashboard toggle Geometry geometry; // [Motor] degrees<->counts maps (yaw + pitch) YawHomeZero yaw_home_zero = YawHomeZero::Off; // [Motor] re-anchor yaw zero on homing ScanConfig scan; // [Scan] grid source ImuConfig imu; // [IMU] Xsens MTi serial device // Capture rate in images/second (derived from general.image_interval). double image_rate() const; }; // Loads and validates configuration. // // Secrets: mqtt_user / mqtt_pw are taken from the environment variables // FGC_MQTT_USER / FGC_MQTT_PW when present; the INI values are a fallback. class ConfigLoader { public: // Reads the INI file at `path` (via the bundled inih parser), maps it into // a typed AppConfig, applies environment overrides, and validates it. // Throws std::runtime_error with a clear message on failure. static AppConfig loadFromFile(const std::string& path); // Same mapping/validation logic, but from an already-parsed key->value map // ("Section.name" => value). Exposed for unit testing without file IO. static AppConfig fromMap(const std::map& kv); }; // Return `contents` with the given `key = value` pairs set under `[section]`: // existing keys in that section are replaced in place (comments/order preserved), // and any missing ones are appended in a `[section]` block at the end. Pure (no // I/O) so it is unit-testable. std::string updateIniSectionKeys(const std::string& contents, const std::string& section, const std::vector>& kv); // Persist the live `[Motor]` calibration (counts_per_deg / zero_count for both // axes) back into the INI file at `path`. Returns true on success. bool saveMotorCalibration(const std::string& path, const Geometry& geo); } // namespace fgc