#pragma once #include #include #include namespace fgc { // Per-axis lifecycle state, from the firmware ST line's state char (B/R/H/A/E). // Per-axis lifecycle, from the firmware ST line's state char. // B Boot R Reset H Homing A reAdy E Error C NeedConfig // // NeedConfig means the firmware has been told nothing about the drivetrain it is // driving and is refusing to move until the host answers its NEED lines. It is // NOT a fault and NOT "no telemetry": the link is fine and the axis is talking. enum class AxisState { Boot, Reset, Homing, Ready, Error, NeedConfig, Unknown }; // One axis segment of a firmware `ST` status line: // ST Y:,,,,,,, [P:...] // Positions are absolute encoder counts; degrees are derived via Geometry. struct AxisTelemetry { AxisState state = AxisState::Unknown; long xactual = 0; // TMC step counter (commanded position) long xenc = 0; // encoder position (actual shaft position) unsigned drv_status = 0; // DRV_STATUS register (decoded from the 8-digit hex) int sg = 0; // SG_RESULT int cs = 0; // CS_ACTUAL int pwm = 0; // PWM_SCALE_SUM bool standstill = false; // flag 'S' (DRV_STST) bool stall = false; // flag 's' (StallGuard) - inert in StealthChop, see below // Flag 'd': the TMC's hardware encoder-deviation latch, set when // |xactual - xenc| exceeded ENC_DEVIATION. This is the usable stall signal on // this rig - StallGuard needs SpreadCycle, which production does not run. // Latched until the axis is homed or reset, so it means "position // untrustworthy", not "stalling right now". bool deviation = false; bool overtemp = false; // flag 'o'/'O' bool endstop_l = false; // flag 'L' bool endstop_r = false; // flag 'R' bool moving() const { return state == AxisState::Homing || !standstill; } bool ready() const { return state == AxisState::Ready; } }; // Telemetry snapshot from the motor controller: one segment per axis. // One "ENV t= h=<%> ok=<0|1> fan= rpm=" line from the firmware: the // DHT11 on the MCU and the enclosure fan it runs (see firmware src/cooling.h). struct McuEnvReading { int temp_c = 0; int humidity = 0; bool sensor_ok = false; // the last read had a good checksum int fan_duty = 0; // 0-255 as driven; 255 is also the failsafe int fan_rpm = 0; // 0 with no tach wired, or a stopped fan long long epoch_ms = 0; // when the host received it }; struct MotorTelemetry { AxisTelemetry yaw; AxisTelemetry pitch; bool pitch_present = false; // was a P: segment seen? // A yaw-only tower: the firmware is talking and its status line has no pitch // segment. Before the first status line nothing is known, so this is false. bool pitchAbsent() const { return yaw.state != AxisState::Unknown && !pitch_present; } }; // Abstraction over the gimbal's motor controller. Implemented by // SerialMotorController (Boost.Asio serial link) and MockMotorController // (simulated, for development without hardware). class IMotorController { public: virtual ~IMotorController() = default; // Begin/*end* background telemetry reading. virtual void start() = 0; virtual void stop() = 0; // Send a command line to the controller (e.g. "HOME", "MOVE Y 20000"). // Implementations append the newline terminator the firmware requires. virtual void sendCommand(const std::string& cmd) = 0; // Latest telemetry snapshot (thread-safe in implementations). virtual MotorTelemetry telemetry() = 0; // The most recently completed firmware DUMP block (the text between // "DUMP BEGIN" and "DUMP END", inclusive), or "" if none has been captured. // Thread-safe in implementations. virtual std::string lastDump() = 0; // The most recently completed firmware DIAG block (the "DG ..." lines from // "DG BEGIN" through "DG DONE"), or "" if none. diagSeq() increments on each // completed capture so callers can detect a fresh result. Thread-safe. virtual std::string lastDiag() = 0; virtual unsigned diagSeq() const = 0; // NEED lines the firmware has emitted since they were last taken, one per // axis, in arrival order (hardware-config-plan.md section 6). Taking them // clears the queue: they are a request to answer once, not a state to poll. // // Default: none. Only the real controller sees the wire; the mock has no // firmware to ask, and a null implementation would have nothing to say. virtual std::vector takeNeedLines() { return {}; } // The latest reading of an ambient DHT11 on the motor MCU and the fan it // drives (firmware "ENV" lines, every 10 s), or nothing on a tower without // one. Thread-safe in implementations. Default: none. virtual std::optional latestEnv() const { return std::nullopt; } // Whether the underlying link is usable. virtual bool connected() const = 0; }; } // namespace fgc