Skip to main content

pamoja_mavlink/protocol/
command.rs

1//! The command protocol: send a command and interpret its acknowledgment.
2//!
3//! A ground station sends a [`CommandLong`](crate::dialect::CommandLong) (or
4//! [`CommandInt`](crate::dialect::CommandInt)) and waits for a
5//! [`CommandAck`] carrying the same command id. The
6//! acknowledgment's result may be
7//! [`IN_PROGRESS`](crate::dialect::mav_result::IN_PROGRESS), which means a long-running
8//! command is still executing and the sender should keep waiting rather than time out. If no
9//! acknowledgment arrives, the command is resent with an incremented `confirmation` count,
10//! up to a retry limit, exactly as the protocol prescribes.
11//!
12//! [`CommandProtocol`] holds that logic: it matches acknowledgments to the command in flight,
13//! classifies each into an [`AckOutcome`], and tracks the `confirmation` count and remaining
14//! retries. It performs no IO; the caller sends the messages and applies the timeout.
15
16use crate::dialect::{mav_result, CommandAck};
17
18/// What an incoming [`CommandAck`] means for the command in flight.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum AckOutcome {
21    /// The acknowledgment was for a different command; ignore it and keep waiting.
22    Unrelated,
23    /// The command is still running; keep waiting. The value is the reported progress
24    /// percent (`0..=100`), or `255` when the autopilot does not report one.
25    InProgress(u8),
26    /// The command finished with this [`MAV_RESULT`](crate::dialect::mav_result) value.
27    Final(u8),
28}
29
30/// Tracks one command awaiting its acknowledgment: which command, the retransmission
31/// `confirmation` count, and the retries left.
32#[derive(Clone, Copy, Debug)]
33pub struct CommandProtocol {
34    command: u16,
35    confirmation: u8,
36    retries_left: u8,
37}
38
39impl CommandProtocol {
40    /// Starts tracking a command, allowing `max_retries` retransmissions.
41    ///
42    /// # Arguments
43    ///
44    /// * `command` - the [`MAV_CMD`](crate::dialect::mav_cmd) id being sent.
45    /// * `max_retries` - how many times the command may be resent after a timeout before the
46    ///   caller gives up.
47    ///
48    /// # Returns
49    ///
50    /// The protocol tracker, with `confirmation` at zero.
51    pub fn new(command: u16, max_retries: u8) -> Self {
52        CommandProtocol {
53            command,
54            confirmation: 0,
55            retries_left: max_retries,
56        }
57    }
58
59    /// Returns the command id being tracked.
60    ///
61    /// # Returns
62    ///
63    /// The command id.
64    pub fn command(&self) -> u16 {
65        self.command
66    }
67
68    /// Returns the `confirmation` count to stamp on the command being sent.
69    ///
70    /// It is zero for the first transmission and increments on each retransmission, which is
71    /// how an autopilot distinguishes a resend from a new command.
72    ///
73    /// # Returns
74    ///
75    /// The current confirmation count.
76    pub fn confirmation(&self) -> u8 {
77        self.confirmation
78    }
79
80    /// Classifies an incoming acknowledgment against the command in flight.
81    ///
82    /// # Arguments
83    ///
84    /// * `ack` - the decoded acknowledgment.
85    ///
86    /// # Returns
87    ///
88    /// [`AckOutcome::Unrelated`] if the ack is for another command,
89    /// [`AckOutcome::InProgress`] if the command is still running, or
90    /// [`AckOutcome::Final`] with the result otherwise.
91    pub fn on_ack(&self, ack: &CommandAck) -> AckOutcome {
92        if ack.command != self.command {
93            return AckOutcome::Unrelated;
94        }
95        if ack.result == mav_result::IN_PROGRESS {
96            AckOutcome::InProgress(ack.progress)
97        } else {
98            AckOutcome::Final(ack.result)
99        }
100    }
101
102    /// Records a timeout and reports whether the command may be resent.
103    ///
104    /// On a resend the `confirmation` count is incremented so the next call to
105    /// [`confirmation`](Self::confirmation) stamps the new value.
106    ///
107    /// # Returns
108    ///
109    /// `Some(confirmation)` with the new count if a retry remains, or [`None`] once the retry
110    /// budget is exhausted.
111    pub fn on_timeout(&mut self) -> Option<u8> {
112        if self.retries_left == 0 {
113            return None;
114        }
115        self.retries_left -= 1;
116        self.confirmation = self.confirmation.wrapping_add(1);
117        Some(self.confirmation)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::dialect::mav_cmd;
125
126    fn ack(command: u16, result: u8, progress: u8) -> CommandAck {
127        CommandAck {
128            command,
129            result,
130            progress,
131            result_param2: 0,
132            target_system: 1,
133            target_component: 1,
134        }
135    }
136
137    #[test]
138    fn an_ack_for_another_command_is_unrelated() {
139        let protocol = CommandProtocol::new(mav_cmd::COMPONENT_ARM_DISARM, 5);
140        let other = ack(mav_cmd::NAV_TAKEOFF, mav_result::ACCEPTED, 0);
141        assert_eq!(protocol.on_ack(&other), AckOutcome::Unrelated);
142    }
143
144    #[test]
145    fn an_accepted_ack_is_final() {
146        let protocol = CommandProtocol::new(mav_cmd::COMPONENT_ARM_DISARM, 5);
147        let accepted = ack(mav_cmd::COMPONENT_ARM_DISARM, mav_result::ACCEPTED, 0);
148        assert_eq!(
149            protocol.on_ack(&accepted),
150            AckOutcome::Final(mav_result::ACCEPTED)
151        );
152    }
153
154    #[test]
155    fn an_in_progress_ack_keeps_waiting_with_the_progress() {
156        let protocol = CommandProtocol::new(mav_cmd::NAV_TAKEOFF, 5);
157        let running = ack(mav_cmd::NAV_TAKEOFF, mav_result::IN_PROGRESS, 42);
158        assert_eq!(protocol.on_ack(&running), AckOutcome::InProgress(42));
159    }
160
161    #[test]
162    fn a_timeout_resends_with_an_incremented_confirmation_until_the_budget_runs_out() {
163        let mut protocol = CommandProtocol::new(mav_cmd::COMPONENT_ARM_DISARM, 2);
164        assert_eq!(protocol.confirmation(), 0);
165        assert_eq!(protocol.on_timeout(), Some(1));
166        assert_eq!(protocol.confirmation(), 1);
167        assert_eq!(protocol.on_timeout(), Some(2));
168        assert_eq!(protocol.confirmation(), 2);
169        // The retry budget is spent; a further timeout gives up.
170        assert_eq!(protocol.on_timeout(), None);
171    }
172}