385 lines
16 KiB
C++
385 lines
16 KiB
C++
#pragma once
|
|
|
|
#include "fgc/IControlChannel.h"
|
|
#include "fgc/ImuMount.h"
|
|
|
|
#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
|
|
double counts_per_deg = 0.0; // live scale, for step-size readouts
|
|
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; // StallGuard ('s') - inert on this rig
|
|
bool deviation = false; // encoder-deviation latch ('d') - the real stall signal
|
|
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 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)
|
|
|
|
// The MTi is not streaming what the host configured (the detail is in
|
|
// ImuConfigView::problems). Flagged on the compact panel too, because the
|
|
// readings above are wrong in a way that reading them cannot reveal.
|
|
bool imu_config_alert = false;
|
|
};
|
|
|
|
// 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.
|
|
// One configured camera, as the expanded view shows it. Everything here is
|
|
// explicit per camera - identity is never implied by position.
|
|
struct CameraView {
|
|
std::string name; // the [Camera.<name>] block
|
|
std::string label; // output subdir + CamEvent "cam"
|
|
std::string id; // USB DEV_ id or GigE IP
|
|
std::string driver;
|
|
bool enabled = true;
|
|
std::string profile; // active profile
|
|
std::vector<std::string> profiles_available;
|
|
std::string profile_dir;
|
|
std::string profile_xml; // the file actually applied to the camera
|
|
int settle_wait_ms = 0;
|
|
int rotation_deg = 0;
|
|
double jxl_distance = 0.0;
|
|
int jxl_effort = 0;
|
|
};
|
|
|
|
struct CameraConfigView {
|
|
bool mock = false;
|
|
std::vector<CameraView> cameras;
|
|
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, temperature_c = 0, actual_fps = 0;
|
|
long long frames_delivered = 0, frames_dropped = 0, restarts = 0;
|
|
};
|
|
|
|
struct CaptureView {
|
|
bool present = false;
|
|
bool active = false;
|
|
// No scan running, so hand aiming (arrows) and `snapshot` are accepted.
|
|
bool manual = true;
|
|
bool snapshot_armed = false; // a snapshot is waiting to fire
|
|
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;
|
|
|
|
// Recent traffic + counters for the expanded 'm' view.
|
|
std::vector<ChannelMessage> recent;
|
|
ChannelStats stats;
|
|
};
|
|
|
|
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;
|
|
|
|
// The [IMU] x/y/z mounting rotation being applied to every reading, already
|
|
// formatted (e.g. "x 180.0 y 0.0 z -90.0"). Empty when no mount is
|
|
// configured. Unlike everything above it comes from config.ini rather than
|
|
// from the device, so the view labels it as such - the MTi has its own
|
|
// internal alignment setting and the two must not be confused.
|
|
std::string mount;
|
|
|
|
// How the device disagreed with what the host asked it for at startup, one
|
|
// finding per line, already formatted. Empty when it agreed.
|
|
//
|
|
// Like `mount` this must survive the config cache, and for a sharper reason:
|
|
// the handshake runs before the TUI installs its log sink, so the LOG_ERROR
|
|
// it emits is wiped by the alt-screen. Without this field the operator would
|
|
// have no way of learning that the IMU is streaming the wrong thing.
|
|
std::vector<std::string> problems;
|
|
};
|
|
|
|
// 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<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;
|
|
double deg_at_center = 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;
|
|
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);
|
|
|
|
// The [IMU] mount as one line: "x 180.0 y 0.0 z -90.0", or "" for no mount.
|
|
// Empty rather than "none" so a caller can tell "not configured" from a value
|
|
// and decide for itself how to say so.
|
|
std::string formatImuMount(const ImuMount& m);
|
|
|
|
// 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);
|
|
|
|
// Whether the dashboard can be sent to the background, i.e. whether it is
|
|
// running inside a tmux pane that a client can be detached from. Pass getenv
|
|
//("TMUX"): tmux sets it to the server socket path, so a set, non-empty value is
|
|
// both the test and what lets `tmux detach-client` find the right client.
|
|
// Takes the value as an argument rather than reading the environment itself so
|
|
// the rule is testable.
|
|
bool backgroundAvailable(const char* tmux_env);
|
|
|
|
// Last path segment of an MQTT topic ("GGS/FWT/Tower/CamEvent" -> "CamEvent").
|
|
// Every row in the connectivity view shares the same prefix, which only eats
|
|
// width. Returns the input unchanged when there is no '/'.
|
|
std::string topicLeaf(const std::string& topic);
|
|
|
|
// "11:56:14.201" (local wall clock) for an absolute ms timestamp; "—" when 0.
|
|
// The message history needs the actual time, not "3s ago": operators correlate
|
|
// it against the broker's own log.
|
|
std::string formatClockMs(long long 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
|