#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 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; }; 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)" / "DHT11 (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 MTi's Temp is the device's *internal* temperature; the DHT11 // Temp (separate group) is *ambient* — deliberately kept apart. struct SensorsView { SensorGroup imu; // Xsens MTi: Roll/Pitch/Yaw + internal temperature SensorGroup dht; // DHT11: ambient temperature + humidity }; // 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 }; struct CaptureView { bool present = false; bool active = false; double image_rate = 0.0; // img/s int camera_count = 0; std::vector labels; // 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) // 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; }; // 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) }; // 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; }; struct UiSnapshot { HeaderView header; GimbalView gimbal; SensorsView sensors; CaptureView capture; ConnView conn; std::vector log; DumpView dump; ImuView imu; ActivityView activity; CalibResultView calib; }; // ---- 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