fwt_software/include/fgc/Config.h

233 lines
11 KiB
C++

#pragma once
#include "fgc/Geometry.h"
#include <map>
#include <string>
#include <utility>
#include <vector>
namespace fgc {
// Typed application configuration. Replaces the ad-hoc std::map<string,string>
// 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<std::string> ids; // GigE IP or USB DEV_ id, in order
std::vector<std::string> labels = {"RGB", "ACR", "NIR"}; // index -> output subfolder
// On-camera imaging (Alvium), applied in-session by VimbaCameraSource at start().
// Defaults keep frames small enough to transfer reliably on the LattePanda USB3 host
// (large ~60 MB frames stall; see docs/known-issues.md "Camera acquisition").
int binning = 2; // 1 = full res; 2 = 2x2 (~5 MP, ~15 MB) default
int offset_x = 0; // ROI origin; 0/0 + 0 size => sensor maximum (full frame)
int offset_y = 0;
int width = 0; // ROI width in pixels; 0 => sensor maximum
int height = 0; // ROI height in pixels; 0 => sensor maximum
std::string pixel_format = "RGB8"; // on-camera de-Bayer + white balance -> colour out
int throughput_mbytes = 250; // DeviceLinkThroughputLimit (MByte/s); do NOT max it
double stream_fps = 1.0; // paced acquisition rate; keeps auto converged. Keep low:
// ~2 fps of 15 MB frames stalls this USB3 host; 1 fps sustains
bool exposure_auto = true; // ExposureAuto = Continuous (adapt to changing light)
double exposure_max_us = 0.0; // ExposureAutoMax cap (us); 0 => leave camera default
bool gain_auto = true; // GainAuto = Continuous
double gain_max_db = 0.0; // GainAutoMax cap (dB); 0 => leave camera default
bool white_balance_auto = true; // BalanceWhiteAuto = Continuous
// JPEG XL encoding (consumed by ImagePipeline). Near-lossless default.
double jxl_distance = 0.8; // 0 = lossless; higher = lossier
int jxl_effort = 4; // libjxl effort 1..9
};
// [Capture]: how a frame is acquired at each waypoint, and the quality bar it has
// to clear. See docs/configuration.md.
struct CaptureConfig {
// "trigger" = one deliberate software-triggered frame per attempt (default).
// "freerun" = the older paced stream + keep-latest, kept as a fallback in case
// software trigger misbehaves on a given host.
std::string mode = "trigger";
// Judge each frame and reshoot with corrected exposure until it passes. With
// this off, `mode` still applies but the first frame is always kept.
bool quality_gate = true;
int max_attempts = 3;
int min_attempts = 1; // >1 always shoots extra and keeps the sharpest
int acquire_timeout_ms = 2000;
int settle_delay_ms = 150; // pause after standstill before the first shot
// Exposure targets (see ExposurePolicy).
double target_mean = 110.0;
double mean_tolerance = 12.0;
double clip_max_fraction = 0.005;
double exposure_min_us = 50.0;
double damping = 0.8;
// Starting point when the store has nothing to offer.
double default_exposure_us = 5000.0;
double default_gain_db = 0.0;
// Blur is judged relative to the same angle's own history; see ExposurePolicy.
double blur_relative_floor = 0.5;
double blur_absolute_floor = 0.0; // 0 disables the absolute backstop
// Metric cost knobs.
int metric_stride = 4;
int sharpness_roi_px = 512;
// Per-angle exposure memory. Empty path => resolved to a default beside the
// logs at load time.
std::string exposure_store;
double angle_quantum_deg = 1.0;
long long store_stale_s = 1800;
};
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
bool enable_env = false; // ambient temp/humidity sensor (off by default)
bool mock_env = false; // use a simulated env sensor instead of the SHT41
};
// [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;
};
// [Env]: ambient temperature/humidity sensor (SHT41) on the LattePanda's own
// native I2C bus — NOT the Arduino/motor serial link. Bus number varies by
// board; confirm with `i2cdetect -y N` (expect the SHT41 at 0x44).
struct EnvConfig {
std::string i2c_device = "/dev/i2c-1"; // empty => required when enabled
unsigned int i2c_addr = 0x44;
int period_ms = 2000; // sample interval
};
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)
};
// [Test] / [TestProfile.<name>]: the hardware self-test command. A profile is a
// named bundle of repetition counts, sampling windows and pass thresholds; the
// modules read the values they care about by key (open map so new tests can add
// knobs without touching the loader).
struct TestProfile {
std::string name;
std::map<std::string, double> params; // numeric knobs: reps, *_window_ms, *_max_*
std::map<std::string, std::string> text; // raw values incl. comma lists (e.g. step sizes)
double get(const std::string& key, double fallback) const;
int geti(const std::string& key, int fallback) const;
bool getBool(const std::string& key, bool fallback) const;
std::string gets(const std::string& key, const std::string& fallback = "") const;
std::vector<long> getLongList(const std::string& key) const; // parse "200,500,1000"
};
struct TestConfig {
std::string default_profile = "standard";
std::string baseline_file; // empty => <logdir>/test_baseline.txt
std::map<std::string, TestProfile> profiles;
const TestProfile* find(const std::string& name) const; // nullptr if absent
};
// 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;
CaptureConfig capture; // [Capture] acquisition mode + image quality gate
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
long enc_error_warn_counts = 400; // [Motor] live encoder-error WARN (0=off)
ScanConfig scan; // [Scan] grid source
ImuConfig imu; // [IMU] Xsens MTi serial device
EnvConfig env; // [Env] SHT41 ambient sensor, LattePanda I2C
TestConfig test; // [Test] hardware self-test profiles
// 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<std::string, std::string>& 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<std::pair<std::string, std::string>>& 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