1pub const MAV_AUTOPILOT_PX4: i64 = 12;
24
25const 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
54const 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
81const 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum ArduPilotFirmware {
99 Copter,
100 Plane,
101 Rover,
102}
103
104impl ArduPilotFirmware {
105 pub fn from_mav_type(mav_type: i64) -> Option<Self> {
110 match mav_type {
111 2 | 3 | 4 | 13 | 14 | 15 | 29 | 35 => Some(Self::Copter),
115 1 | 19..=24 => Some(Self::Plane),
118 10 | 11 => Some(Self::Rover),
120 _ => None,
121 }
122 }
123
124 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 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 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 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 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 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 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 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 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 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 let copter = ArduPilotFirmware::from_banner("ArduCopter V4.6.0 (1234abcd)");
262 assert_eq!(
263 ArduPilotFirmware::resolve(copter, 2),
264 Some(ArduPilotFirmware::Copter)
265 );
266 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 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}