Skip to main content

ados_protocol/
flight_modes.rs

1//! ArduPilot flight-mode tables, keyed by the firmware the FC runs.
2//!
3//! An ArduPilot `custom_mode` number means a different mode on each firmware:
4//! `6` is RTL on Copter, FBWB on Plane and FOLLOW on Rover. The mode name the
5//! state snapshot reports (decode) and the `custom_mode` a `DO_SET_MODE` command
6//! carries (encode) must therefore both come from the table for the firmware
7//! that is actually connected. This module is the one copy of those tables: the
8//! MAVLink router decodes with it and the control surface encodes with it, so
9//! the two can never disagree.
10//!
11//! `HEARTBEAT.type` (MAV_TYPE) alone cannot name the firmware: ArduPlane
12//! reports `Q_MAV_TYPE` for a QuadPlane, so a plane can arrive as a
13//! QUADROTOR. The firmware is identified from the banner STATUSTEXT ArduPilot
14//! sends at boot and on every `PARAM_REQUEST_LIST` ([`ArduPilotFirmware::from_banner`]);
15//! until then only a MAV_TYPE that just one firmware reports is trusted
16//! ([`ArduPilotFirmware::resolve`]).
17//!
18//! PX4 packs its mode into `custom_mode` differently and is keyed on
19//! `HEARTBEAT.autopilot` ([`MAV_AUTOPILOT_PX4`]), not on MAV_TYPE.
20
21/// `HEARTBEAT.autopilot` for PX4 (`MAV_AUTOPILOT_PX4`). Any other autopilot
22/// value is decoded and encoded through the ArduPilot tables below.
23pub const MAV_AUTOPILOT_PX4: i64 = 12;
24
25/// ArduCopter `custom_mode` -> mode name.
26const COPTER_MODES: &[(u32, &str)] = &[
27    (0, "STABILIZE"),
28    (1, "ACRO"),
29    (2, "ALT_HOLD"),
30    (3, "AUTO"),
31    (4, "GUIDED"),
32    (5, "LOITER"),
33    (6, "RTL"),
34    (7, "CIRCLE"),
35    (9, "LAND"),
36    (11, "DRIFT"),
37    (13, "SPORT"),
38    (14, "FLIP"),
39    (15, "AUTOTUNE"),
40    (16, "POSHOLD"),
41    (17, "BRAKE"),
42    (18, "THROW"),
43    (19, "AVOID_ADSB"),
44    (20, "GUIDED_NOGPS"),
45    (21, "SMART_RTL"),
46    (22, "FLOWHOLD"),
47    (23, "FOLLOW"),
48    (24, "ZIGZAG"),
49    (25, "SYSTEMID"),
50    (26, "AUTOROTATE"),
51    (27, "AUTO_RTL"),
52];
53
54/// ArduPlane (including QuadPlane) `custom_mode` -> mode name.
55const PLANE_MODES: &[(u32, &str)] = &[
56    (0, "MANUAL"),
57    (1, "CIRCLE"),
58    (2, "STABILIZE"),
59    (3, "TRAINING"),
60    (4, "ACRO"),
61    (5, "FBWA"),
62    (6, "FBWB"),
63    (7, "CRUISE"),
64    (8, "AUTOTUNE"),
65    (10, "AUTO"),
66    (11, "RTL"),
67    (12, "LOITER"),
68    (14, "AVOID_ADSB"),
69    (15, "GUIDED"),
70    (17, "QSTABILIZE"),
71    (18, "QHOVER"),
72    (19, "QLOITER"),
73    (20, "QLAND"),
74    (21, "QRTL"),
75    (22, "QAUTOTUNE"),
76    (23, "QACRO"),
77    (24, "THERMAL"),
78    (25, "LOITER_ALT_QLAND"),
79];
80
81/// ArduRover (ground and boat frames) `custom_mode` -> mode name.
82const ROVER_MODES: &[(u32, &str)] = &[
83    (0, "MANUAL"),
84    (1, "ACRO"),
85    (3, "STEERING"),
86    (4, "HOLD"),
87    (5, "LOITER"),
88    (6, "FOLLOW"),
89    (7, "SIMPLE"),
90    (10, "AUTO"),
91    (11, "RTL"),
92    (12, "SMART_RTL"),
93    (15, "GUIDED"),
94];
95
96/// The ArduPilot firmware a MAV_TYPE identifies, which selects the mode table.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum ArduPilotFirmware {
99    Copter,
100    Plane,
101    Rover,
102}
103
104impl ArduPilotFirmware {
105    /// The firmware a `HEARTBEAT.type` (MAV_TYPE wire value) suggests, or
106    /// `None` for a type with no mode table here (generic, submarine, airship,
107    /// antenna tracker, ...). A multirotor type is only a suggestion: a
108    /// QuadPlane reports one too. Encoders use [`Self::resolve`].
109    pub fn from_mav_type(mav_type: i64) -> Option<Self> {
110        match mav_type {
111            // COAXIAL, HELICOPTER, QUADROTOR, HEXAROTOR, OCTOROTOR, TRICOPTER,
112            // DODECAROTOR, DECAROTOR: the frames ArduCopter reports, and the
113            // Q_MAV_TYPE values a QuadPlane may report.
114            2 | 3 | 4 | 13 | 14 | 15 | 29 | 35 => Some(Self::Copter),
115            // FIXED_WING and the VTOL types (tailsitter duo/quad, tiltrotor,
116            // fixed-rotor, tailsitter, tiltwing): ArduPlane and QuadPlane.
117            1 | 19..=24 => Some(Self::Plane),
118            // GROUND_ROVER, SURFACE_BOAT.
119            10 | 11 => Some(Self::Rover),
120            _ => None,
121        }
122    }
123
124    /// The firmware a banner STATUSTEXT names (`"ArduPlane V4.5.7 (…)"`), or
125    /// `None` for any other text.
126    pub fn from_banner(text: &str) -> Option<Self> {
127        match text.split_whitespace().next()? {
128            "ArduCopter" | "APM:Copter" => Some(Self::Copter),
129            "ArduPlane" | "APM:Plane" => Some(Self::Plane),
130            "ArduRover" | "APM:Rover" => Some(Self::Rover),
131            _ => None,
132        }
133    }
134
135    /// The mode table to use for a vehicle: the identified firmware when the
136    /// banner has been seen; otherwise the firmware a MAV_TYPE only it reports
137    /// names. A multirotor MAV_TYPE with no banner is `None`, because it may be
138    /// a QuadPlane, where the copter RTL number commands FBWB.
139    pub fn resolve(identified: Option<Self>, mav_type: i64) -> Option<Self> {
140        identified.or(match Self::from_mav_type(mav_type) {
141            Some(Self::Copter) => None,
142            other => other,
143        })
144    }
145
146    /// The snapshot's name for this firmware.
147    pub fn as_str(self) -> &'static str {
148        match self {
149            Self::Copter => "copter",
150            Self::Plane => "plane",
151            Self::Rover => "rover",
152        }
153    }
154
155    /// The firmware a snapshot name ([`Self::as_str`]) denotes.
156    pub fn from_name(name: &str) -> Option<Self> {
157        match name {
158            "copter" => Some(Self::Copter),
159            "plane" => Some(Self::Plane),
160            "rover" => Some(Self::Rover),
161            _ => None,
162        }
163    }
164
165    /// The `custom_mode` -> name table for this firmware.
166    pub fn modes(self) -> &'static [(u32, &'static str)] {
167        match self {
168            Self::Copter => COPTER_MODES,
169            Self::Plane => PLANE_MODES,
170            Self::Rover => ROVER_MODES,
171        }
172    }
173
174    /// The name of `custom_mode` on this firmware, `None` when unmapped.
175    pub fn mode_name(self, custom_mode: u32) -> Option<&'static str> {
176        self.modes()
177            .iter()
178            .find(|(num, _)| *num == custom_mode)
179            .map(|(_, name)| *name)
180    }
181
182    /// The `custom_mode` for an upper-case mode name on this firmware, `None`
183    /// when the firmware has no such mode.
184    pub fn custom_mode(self, name: &str) -> Option<u32> {
185        self.modes()
186            .iter()
187            .find(|(_, n)| *n == name)
188            .map(|(num, _)| *num)
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    const ALL: [ArduPilotFirmware; 3] = [
197        ArduPilotFirmware::Copter,
198        ArduPilotFirmware::Plane,
199        ArduPilotFirmware::Rover,
200    ];
201
202    #[test]
203    fn mav_type_selects_the_firmware_table() {
204        for t in [2, 3, 4, 13, 14, 15, 29, 35] {
205            assert_eq!(
206                ArduPilotFirmware::from_mav_type(t),
207                Some(ArduPilotFirmware::Copter),
208                "{t}"
209            );
210        }
211        for t in [1, 19, 20, 21, 22, 23, 24] {
212            assert_eq!(
213                ArduPilotFirmware::from_mav_type(t),
214                Some(ArduPilotFirmware::Plane),
215                "{t}"
216            );
217        }
218        for t in [10, 11] {
219            assert_eq!(
220                ArduPilotFirmware::from_mav_type(t),
221                Some(ArduPilotFirmware::Rover),
222                "{t}"
223            );
224        }
225        // GENERIC, ANTENNA_TRACKER, AIRSHIP, SUBMARINE, VTOL_RESERVED5.
226        for t in [0, 5, 7, 12, 25, -1] {
227            assert_eq!(ArduPilotFirmware::from_mav_type(t), None, "{t}");
228        }
229    }
230
231    #[test]
232    fn rtl_differs_by_firmware() {
233        assert_eq!(ArduPilotFirmware::Copter.custom_mode("RTL"), Some(6));
234        assert_eq!(ArduPilotFirmware::Plane.custom_mode("RTL"), Some(11));
235        assert_eq!(ArduPilotFirmware::Rover.custom_mode("RTL"), Some(11));
236        // The copter RTL number is a manual mode elsewhere.
237        assert_eq!(ArduPilotFirmware::Plane.mode_name(6), Some("FBWB"));
238        assert_eq!(ArduPilotFirmware::Rover.mode_name(6), Some("FOLLOW"));
239    }
240
241    #[test]
242    fn every_table_is_a_bijection() {
243        for fw in ALL {
244            for (num, name) in fw.modes() {
245                assert_eq!(fw.custom_mode(name), Some(*num), "{fw:?} {name}");
246                assert_eq!(fw.mode_name(*num), Some(*name), "{fw:?} {num}");
247            }
248        }
249    }
250
251    #[test]
252    fn a_quadplane_reporting_a_multirotor_type_is_never_given_the_copter_table() {
253        // Q_MAV_TYPE=QUADROTOR: the heartbeat says 2. Without the banner the
254        // firmware is unknown, so no mode number is guessed.
255        assert_eq!(ArduPilotFirmware::resolve(None, 2), None);
256        let plane = ArduPilotFirmware::from_banner("ArduPlane V4.5.7 (4a6ff3b2)");
257        assert_eq!(plane, Some(ArduPilotFirmware::Plane));
258        let fw = ArduPilotFirmware::resolve(plane, 2).unwrap();
259        assert_eq!(fw.custom_mode("RTL"), Some(11), "never the copter 6 (FBWB)");
260        // A copter is a copter once its banner says so.
261        let copter = ArduPilotFirmware::from_banner("ArduCopter V4.6.0 (1234abcd)");
262        assert_eq!(
263            ArduPilotFirmware::resolve(copter, 2),
264            Some(ArduPilotFirmware::Copter)
265        );
266        // A type only one firmware reports needs no banner.
267        assert_eq!(
268            ArduPilotFirmware::resolve(None, 1),
269            Some(ArduPilotFirmware::Plane)
270        );
271        assert_eq!(
272            ArduPilotFirmware::resolve(None, 10),
273            Some(ArduPilotFirmware::Rover)
274        );
275        // Other text names nothing.
276        assert_eq!(ArduPilotFirmware::from_banner("ChibiOS: 1a2b3c"), None);
277        assert_eq!(ArduPilotFirmware::from_banner(""), None);
278        for fw in ALL {
279            assert_eq!(ArduPilotFirmware::from_name(fw.as_str()), Some(fw));
280        }
281    }
282}