#pragma once #include "fgc/Geometry.h" #include #include #include #include #include #include namespace fgc { class IMotorController; class IImuSource; // Live progress of a running calibration, for the TUI activity strip. struct CalibProgress { bool running = false; char axis = '?'; // 'Y' / 'P' int step = 0; // 1-based current step int total = 0; // steps per axis std::string phase; // "moving" / "dwelling" / "fitting" / ... }; // Structured outcome of the last completed calibration (persists for display). struct CalibReport { struct Axis { char axis = '?'; bool ok = false; double counts_per_deg = 0; long zero_count = 0; double r2 = 0; int n = 0; }; bool valid = false; long long ts_ms = 0; // wall-clock completion time (epoch ms) bool all_ok = false; std::vector axes; }; // Tunable timings/sizes for a calibration run. Defaults are the production // values; tests inject tiny ones so a full run completes in milliseconds. struct CalibParams { int positions = 10; // sweep steps per axis int dwell_ms = 5000; // hold at each position while sampling int sample_ms = 100; // IMU sampling cadence during a dwell int settle_timeout_ms = 15000; // max wait for a single MOVE to settle int home_timeout_ms = 90000; // max wait for HOME to reach READY int norotation_s = 3; // no-rotation gyro-bias update duration int reset_settle_ms = 1500; // let the filter apply the heading reset double inset_frac = 0.05; // keep targets off the hard endstops }; // `gimbal calib`: an IMU-referenced steps<->degrees calibration. Runs on its own // thread (the motor/imu/Logger interfaces are thread-safe). Sequence: // 1. home the gimbal if it is not already READY; // 2. move yaw to its first sweep position and calibrate PITCH there (sweep its // soft-limit travel, dwelling to record the gravity-referenced IMU pitch); // 3. move pitch to 0 deg; // 4. switch the IMU to a no-magnetometer XKF profile (persists on the device) so // its heading stops chasing the stepper-distorted magnetic field; // 5. with the gimbal held still, run the IMU no-rotation update (cuts yaw drift) // then reset the IMU heading so the current pose is yaw 0 — this removes the // drift accumulated during the slow pitch sweep right before it matters; // 6. sweep YAW from that first position and calibrate it. // Each axis fit is least-squares (counts vs degrees). The resulting Geometry is // published via takeResult() for the main thread to apply; raw samples + fits are // written to a logfile. Progress streams to the LOG pane. Cancellable, one-at-a-time. class CalibrationRoutine { public: CalibrationRoutine(IMotorController& motor, IImuSource& imu, Geometry initial, CalibParams params = {}); ~CalibrationRoutine(); // Begin on a worker thread. Logs a reason and returns false if already // running (prechecks happen on the worker and abort there). bool start(); void cancel(); bool running() const { return running_.load(); } // If a finished run produced a new calibration, returns it once (then clears). std::optional takeResult(); // Live progress (thread-safe copy) and the last completed report (persists). CalibProgress progress() const; CalibReport report() const; private: void run(); void setProgress(char axis, int step, int total, const char* phase); IMotorController& motor_; IImuSource& imu_; Geometry initial_; CalibParams params_; std::thread thread_; std::atomic running_{false}; std::atomic cancel_{false}; mutable std::mutex result_mutex_; std::optional result_; CalibReport report_; mutable std::mutex progress_mutex_; CalibProgress progress_; }; } // namespace fgc