#pragma once #include "fgc/IMotorController.h" // AxisState #include "fgc/Logger.h" // LogLevel #include #include namespace fgc { // Plain-data view model handed from the control loop to a user interface. It is // a pure snapshot: no references to the live motor/camera/channel objects, so // the UI thread can copy and render it without touching (or racing) application // logic. New subsystems become new structs here + a panel in the TUI. // Colour intent, mapped to concrete terminal colours by the renderer. enum class UiColor { Default, Dim, Green, Yellow, Red, Cyan }; // One gimbal axis. struct AxisView { std::string label; // "YAW" / "PITCH" AxisState state = AxisState::Unknown; double deg = 0.0; // heading/elevation, from xenc via Geometry long xactual = 0; long xenc = 0; long enc_err = 0; // live tracking error = xactual - xenc (counts) double target_deg = 0.0; // current scheduler target int sg = 0; // SG_RESULT (live, from ST line) int cs = 0; // CS_ACTUAL int pwm = 0; // PWM_SCALE_SUM unsigned drv_status = 0; // DRV_STATUS register bool moving = false; bool standstill = false; bool stall = false; bool overtemp = false; bool endstop_l = false; bool endstop_r = false; // Homing travel limits (from the last firmware dump): the endstop positions // found during homing, in encoder counts and converted to degrees. bool has_limits = false; long lim_neg = 0; long lim_pos = 0; double lim_neg_deg = 0.0; double lim_pos_deg = 0.0; // Encoder-tracking health (from the last firmware dump): disturbances the // firmware hold corrector counted/erased. has_track gates rendering. bool has_track = false; long hold_corrections = 0; long hold_peak_dev = 0; long hold_cumulative_dev = 0; }; struct GimbalView { bool present = false; // motor link usable AxisView yaw; AxisView pitch; bool pitch_present = false; }; // One labelled sensor reading. `present=false` => render the dim "pending" form. struct SensorField { std::string label; // "Temp", "Humid", "Roll"... std::string value; // formatted value, or placeholder when absent std::string unit; // "°C", "%RH", "°"... bool present = false; }; // A labelled group of readings from one physical sensor, rendered as its own // subsection in the Sensors panel. struct SensorGroup { std::string title; // "MTi (orientation)" / "SHT41 (ambient)" std::string status = "pending"; // "live" / "pending" / "no fix" bool present = false; // true once the driver feeds real data std::vector fields; }; // The Sensors panel, split by source so it is clear which parameters come from // which device. Each group is filled with pending placeholders until its driver // lands. NOTE: the only temperature here is the env sensor's *ambient* one — the // MTi's *internal* device temperature is diagnostic detail and lives solely in // the expanded view (ImuView::temp_c), so the compact panel never shows two // temperatures the operator has to tell apart. The env group's title/backing // sensor is swappable (currently SHT41 over I2C; see IEnvSensor) without // touching this struct. struct SensorsView { SensorGroup imu; // Xsens MTi: Roll/Pitch/Yaw SensorGroup env; // ambient temperature + humidity (SHT41) }; // One auto-sweep scan-grid waypoint, for the expanded camera view. struct ScanWaypointView { double yaw_deg = 0.0; double pitch_deg = 0.0; bool current = false; // the scheduler's live cursor position }; // Static imaging + encoding config (from CameraConfig), for the expanded view. struct CameraConfigView { bool mock = false; std::vector ids; std::string pixel_format; int binning = 1; int offset_x = 0, offset_y = 0, width = 0, height = 0; // 0 = full/max int throughput_mbytes = 0; double stream_fps = 0.0; bool exposure_auto = true; double exposure_max_us = 0.0; bool gain_auto = true; double gain_max_db = 0.0; bool white_balance_auto = true; double jxl_distance = 0.0; int jxl_effort = 0; std::string output_dir; }; // Live per-camera identity/telemetry/stats (from ICameraSource::deviceInfo()). struct CameraDeviceView { std::string id, model, serial; bool streaming = false; unsigned width = 0, height = 0; long long payload_bytes = 0; double exposure_us = 0, gain_db = 0, wb_red = 0, wb_blue = 0, temperature_c = 0, actual_fps = 0; long long frames_delivered = 0, frames_dropped = 0, restarts = 0; }; struct CaptureView { bool present = false; bool active = false; double image_rate = 0.0; // img/s int camera_count = 0; std::vector labels; // Full camera-system detail for the expanded ('c') view. CameraConfigView config; std::vector devices; long long images_saved = 0; // Defined scan grid (ControlCode 0 auto-sweep waypoints) with the live cursor // flagged, surfaced in the expanded camera view. std::vector scan_grid; bool scan_from_file = false; // grid_file CSV vs generated bool scan_pitch = false; // 2-axis rig (show pitch column) std::string scan_error; // grid_file load failure (auto-sweep off) // Outcome of the last gated capture (from GatedCameraSource::lastReport()). // `gate_active` is false when the quality gate is off, in which case the rest // of these are meaningless. bool gate_active = false; bool gate_degraded = false; // budget exhausted; best effort was kept int gate_attempts = 0; std::string gate_reason; // "ok" / "clipped" / "dark" / "blur" / ... double gate_mean_luma = 0.0; double gate_clipped_pct = 0.0; double gate_sharpness = 0.0; double gate_exposure_us = 0.0; double gate_gain_db = 0.0; long long gate_degraded_total = 0; // degraded captures this session // Last published capture (from ImagePipeline::lastEvent()). bool has_last = false; std::string last_label; double last_heading_deg = 0.0; double last_pitch_deg = 0.0; long long last_ts_ms = 0; }; struct ConnView { bool mqtt_enabled = false; bool mqtt_connected = false; std::string broker; std::string tower; int control_code = 0; // 0 = auto-sweep, 1 = directed std::string target_heading; int last_status_code = 0; }; struct HeaderView { std::string tower; std::string build; long long uptime_ms = 0; bool live = true; // LIVE vs MOCK }; struct LogLine { LogLevel level = LogLevel::Info; std::string text; // formatted line (no trailing newline) }; // Most recent firmware DUMP block (raw text), surfaced in the TUI help pane. struct DumpView { bool has = false; std::string text; }; // Device configuration read back from the MTi at startup, formatted for the // expanded Sensors view. `present` is false until the handshake reports it (or // for backends that cannot report it). // One XKF (Xsens Kalman Filter) profile the device supports, with whether it is // the active one. struct ImuProfileView { std::string name; // human label, no numeric IDs ("General") bool selected = false; // the profile currently in use }; struct ImuConfigView { bool present = false; std::string product_code; // "MTi-28A53G35" std::string device_id; // "0x00990ABC" std::string firmware; // "2.8.1 build 0" std::string output_mode; // "Temp · Calibrated · Orientation" std::string output_settings; // "Euler · Sample counter · Float" std::string channels; // "acc gyr mag" std::string sample_rate; // "100 Hz" // Available XKF profiles with the active one flagged. Empty if the device // did not report them. std::vector xkf_profiles; }; // Full Xsens MTi reading for the expanded Sensors view (units: acc m/s^2, // gyr rad/s, mag a.u., angles deg, temp °C). struct ImuView { bool present = false; float roll_deg = 0, pitch_deg = 0, yaw_deg = 0; float acc[3] = {0, 0, 0}; float gyr[3] = {0, 0, 0}; float mag[3] = {0, 0, 0}; float temp_c = 0; unsigned sample_counter = 0; ImuConfigView config; }; // Full ambient-sensor reading for the expanded Sensors view. This temperature is // *ambient*, as distinct from ImuView::temp_c (the MTi's device-internal temp); // the expanded view labels both so they are never confused. struct EnvView { bool enabled = false; // driver constructed (enable_env / --mock-env) bool present = false; // a fresh (non-stale) sample is available bool mock = false; // MockEnvSensor: the I2C bus config is meaningless std::string name; // IEnvSensor::name() — "SHT41" / "SHT41 (mock)" float temp_c = 0, humidity_pct = 0; long long timestamp_ms = 0; // epoch ms, for formatTimeAgo() std::string i2c_device; // from EnvConfig, for the bus row unsigned i2c_addr = 0; int period_ms = 0; }; // Status of the currently-running special operation (calibration, diagnostics, // homing, capture) plus the last completed result, for the activity strip below // the log. The result block persists until replaced so it never scrolls away. struct ActivityView { bool active = false; // a special op is running now std::string title; // "CALIBRATING" / "DIAGNOSTICS" / "HOMING" / "CAPTURE" std::string status; // live one-line progress bool has_result = false; std::string result_title; // e.g. "Calibration · 2m ago" std::vector result; // summary lines UiColor result_color = UiColor::Default; // green pass / red fail std::string prompt; // a yes/no question awaiting the operator (empty = none) bool cancelable = false; // a procedure is running and Esc cancels it }; // Last calibration fit, per axis, for the gimbal expanded view. struct CalibAxisView { bool ok = false; double counts_per_deg = 0; long zero_count = 0; double r2 = 0; int n = 0; }; struct CalibResultView { bool has = false; long long ts_ms = 0; bool pitch_present = false; CalibAxisView yaw, pitch; }; // Worst-case encoder tracking error from the last DIAG run, per axis, for the // gimbal expanded view. err_* are the largest values across that axis's tests // (-1 when the axis has no encoder). struct DiagAxisView { char axis = '?'; bool has = false; bool pass = false; long err_peak = 0; // worst following error during the moves (counts) long err_rms = 0; // worst RMS following error (counts) long err_still = 0; // worst standstill error (counts) }; struct DiagResultView { bool has = false; long long ts_ms = 0; std::vector axes; // Y and (if present) P }; // Outcome of the last `test` (hardware self-test) run, for the activity strip. struct TestResultView { bool has = false; long long ts_ms = 0; bool pass = false; std::string profile; std::vector summary; // per-section PASS/FAIL lines }; struct UiSnapshot { HeaderView header; GimbalView gimbal; SensorsView sensors; CaptureView capture; ConnView conn; std::vector log; DumpView dump; ImuView imu; EnvView env; ActivityView activity; CalibResultView calib; DiagResultView diag; TestResultView test; }; // ---- Pure formatting helpers (unit-tested in tests/test_uisnapshot.cpp) ---- // Short state label for an axis state char (B/R/H/A/E -> these strings). const char* axisStateLabel(AxisState s); // Colour intent for an axis state (READY=green, HOMING=yellow, ERROR=red, // BOOT/RESET=dim, Unknown=default). UiColor axisStateColor(AxisState s); // "12.3°" (one decimal, sign preserved). std::string formatDegrees(double deg); // Human "Ns ago" / "Nm ago" for an absolute ms timestamp relative to now_ms. // Returns "—" when then_ms is 0 (never). std::string formatTimeAgo(long long now_ms, long long then_ms); // MVP placeholder: the Sensors panel fields shown before the DHT11/MTi drivers // exist (all `present=false`). Centralised so the panel and tests agree. SensorsView pendingSensorsView(); } // namespace fgc