fwt_software/include/fgc/ui/UiSnapshot.h

301 lines
12 KiB
C++

#pragma once
#include "fgc/IMotorController.h" // AxisState
#include "fgc/Logger.h" // LogLevel
#include <string>
#include <vector>
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<SensorField> 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 env
// sensor's Temp (separate group) is *ambient* — deliberately kept 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 + internal temperature
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<std::string> 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<std::string> labels;
// Full camera-system detail for the expanded ('c') view.
CameraConfigView config;
std::vector<CameraDeviceView> 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<ScanWaypointView> 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)
// 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<ImuProfileView> 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<std::string> 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<DiagAxisView> 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<std::string> summary; // per-section PASS/FAIL lines
};
struct UiSnapshot {
HeaderView header;
GimbalView gimbal;
SensorsView sensors;
CaptureView capture;
ConnView conn;
std::vector<LogLine> log;
DumpView dump;
ImuView imu;
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