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}