228 lines
10 KiB
C++
228 lines
10 KiB
C++
#pragma once
|
|
|
|
#include "fgc/ImuMount.h"
|
|
#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 host/IP, or a full tcp://... URI
|
|
int mqtt_port = 1883; // ignored when broker_ip carries its own port
|
|
std::string mqtt_user; // see secrets note below
|
|
std::string mqtt_pw;
|
|
|
|
// Paho serverURI for broker_ip + mqtt_port, e.g. "tcp://10.0.0.5:1883".
|
|
//
|
|
// Built here rather than handing Paho a bare address and relying on its
|
|
// default port, so the port is explicit, configurable and visible in the log.
|
|
// Two precedence rules, both to keep existing configs working:
|
|
// - broker_ip containing "://" is a URI the operator owns: returned verbatim,
|
|
// mqtt_port ignored (it may already have a port, or a ws:// path to break).
|
|
// - broker_ip already carrying "host:port" keeps that port; mqtt_port ignored.
|
|
// A bare IPv6 address is bracketed, or Paho's rightmost-colon parse would read
|
|
// the last hextet as the port.
|
|
std::string brokerUri() const;
|
|
};
|
|
|
|
struct SerialConfig {
|
|
std::string device = "/dev/ttyACM0";
|
|
unsigned int baud = 115200;
|
|
};
|
|
|
|
// Per-profile program behaviour, from [Camera.<name>.<profile>]. These are not
|
|
// camera settings - the camera's own settings live entirely in the profile XML -
|
|
// but timing the program must observe around them.
|
|
struct CameraProfileTiming {
|
|
int settle_wait_ms = 1000; // hold after standstill before keeping a frame
|
|
int frame_timeout_ms = 2000; // give up on the waypoint after this
|
|
};
|
|
|
|
// One camera, from a [Camera.<name>] block. Nothing is implied by position: the
|
|
// block names the camera, `label` names its output, `driver` picks the backend.
|
|
struct CameraSpec {
|
|
std::string name; // the block suffix, e.g. "rgb"
|
|
bool enabled = true;
|
|
std::string driver = "vimba"; // only "vimba" is implemented
|
|
std::string id; // USB DEV_ id or GigE IP
|
|
std::string label; // output subdir + CamEvent "cam"; defaults to name
|
|
std::string profile_dir; // relative => resolved next to config.ini
|
|
std::string profile = "standard"; // which profile to start in
|
|
int rotation_deg = 0; // clockwise; 0/90/180/270
|
|
|
|
// Encoding, defaulted from [Camera] and overridable per camera.
|
|
double jxl_distance = 0.8;
|
|
int jxl_effort = 4;
|
|
|
|
std::map<std::string, CameraProfileTiming> timing; // by profile name
|
|
|
|
// Timing for `profile`, or the defaults when the block does not name it.
|
|
CameraProfileTiming timingFor(const std::string& profile_name) const;
|
|
};
|
|
|
|
struct CameraConfig {
|
|
std::vector<CameraSpec> cameras; // in config order; empty is a config error
|
|
// [Camera] defaults, inherited by each block unless it overrides them.
|
|
double jxl_distance = 0.8;
|
|
int jxl_effort = 4;
|
|
|
|
// Enabled cameras only.
|
|
std::vector<const CameraSpec*> active() const;
|
|
// The gimbal must stand still long enough for EVERY active camera, so the
|
|
// scheduler takes the longest wait rather than the first camera's.
|
|
int maxSettleWaitMs() const;
|
|
int maxFrameTimeoutMs() const;
|
|
};
|
|
|
|
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;
|
|
|
|
// How the unit is bolted to the gimbal, as [IMU] x/y/z. Every reading the
|
|
// program produces is rotated into the gimbal's NED body frame by this, so
|
|
// nothing downstream ever sees the sensor frame. See ImuMount.h for the
|
|
// convention; the default is the identity, which changes nothing.
|
|
ImuMount mount;
|
|
};
|
|
|
|
// [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;
|
|
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
|