#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; double target_deg = 0.0; // current scheduler target bool moving = false; bool standstill = false; bool stall = false; bool overtemp = false; bool endstop_l = false; bool endstop_r = false; }; 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; }; // DHT11 (temperature/humidity) + Xsens MTi (orientation). Neither is integrated // yet, so the MVP fills `fields` with pending placeholders; flip the *_present // flags and populate values when the drivers land. struct SensorsView { bool dht_present = false; bool imu_present = false; std::vector fields; }; struct CaptureView { bool present = false; bool active = false; double image_rate = 0.0; // img/s int camera_count = 0; std::vector labels; // 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; }; struct UiSnapshot { HeaderView header; GimbalView gimbal; SensorsView sensors; CaptureView capture; ConnView conn; std::vector log; DumpView dump; }; // ---- 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