1use serde::Serialize;
2use std::{convert::Infallible, str::FromStr};
3
4use crate::utils::split_value_unit;
5#[derive(Debug, PartialEq, Eq, Default, Clone, Serialize)]
6pub struct AdditionalPrecision {
7 pub lat: u8,
8 pub lon: u8,
9}
10
11#[derive(Debug, PartialEq, Eq, Default, Clone, Serialize)]
12pub struct ID {
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub reserved: Option<u16>,
15 pub address_type: u16,
16 pub aircraft_type: u8,
17 pub is_stealth: bool,
18 pub is_notrack: bool,
19 pub address: u32,
20}
21
22#[derive(Debug, PartialEq, Default, Clone, Serialize)]
23pub struct PositionComment {
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub course: Option<u16>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub speed: Option<u16>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub altitude: Option<u32>,
30 #[serde(skip_serializing)]
31 pub additional_precision: Option<AdditionalPrecision>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 #[serde(flatten)]
34 pub id: Option<ID>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub climb_rate: Option<i16>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub turn_rate: Option<f32>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub signal_quality: Option<f32>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub error: Option<u8>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub frequency_offset: Option<f32>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub gps_quality: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub flight_level: Option<f32>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub signal_power: Option<f32>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub software_version: Option<f32>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub hardware_version: Option<u8>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub original_address: Option<u32>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub unparsed: Option<String>,
59}
60
61impl FromStr for PositionComment {
62 type Err = Infallible;
63 fn from_str(s: &str) -> Result<Self, Self::Err> {
64 let mut position_comment = PositionComment {
65 ..Default::default()
66 };
67 let mut unparsed: Vec<_> = vec![];
68 for (idx, part) in s.split_ascii_whitespace().enumerate() {
69 if idx == 0 && part.len() == 16 && position_comment.course.is_none() {
74 let subparts = part.split('/').collect::<Vec<_>>();
75 let course = subparts[0].parse::<u16>().ok();
76 let speed = subparts[1].parse::<u16>().ok();
77 let altitude = if &subparts[2][0..2] == "A=" {
78 subparts[2][2..].parse::<u32>().ok()
79 } else {
80 None
81 };
82 if course.is_some()
83 && course.unwrap() <= 360
84 && speed.is_some()
85 && altitude.is_some()
86 {
87 position_comment.course = course;
88 position_comment.speed = speed;
89 position_comment.altitude = altitude;
90 } else {
91 unparsed.push(part);
92 }
93 } else if idx == 0
96 && part.len() == 9
97 && &part[0..3] == "/A="
98 && position_comment.altitude.is_none()
99 {
100 match part[3..].parse::<u32>().ok() {
101 Some(altitude) => position_comment.altitude = Some(altitude),
102 None => unparsed.push(part),
103 }
104 } else if idx == 1
108 && part.len() == 5
109 && &part[0..2] == "!W"
110 && &part[4..] == "!"
111 && position_comment.additional_precision.is_none()
112 {
113 let add_lat = part[2..3].parse::<u8>().ok();
114 let add_lon = part[3..4].parse::<u8>().ok();
115 match (add_lat, add_lon) {
116 (Some(add_lat), Some(add_lon)) => {
117 position_comment.additional_precision = Some(AdditionalPrecision {
118 lat: add_lat,
119 lon: add_lon,
120 })
121 }
122 _ => unparsed.push(part),
123 }
124 } else if part.len() == 10 && &part[0..2] == "id" && position_comment.id.is_none() {
133 if let (Some(detail), Some(address)) = (
134 u8::from_str_radix(&part[2..4], 16).ok(),
135 u32::from_str_radix(&part[4..10], 16).ok(),
136 ) {
137 let address_type = (detail & 0b0000_0011) as u16;
138 let aircraft_type = (detail & 0b_0011_1100) >> 2;
139 let is_notrack = (detail & 0b0100_0000) != 0;
140 let is_stealth = (detail & 0b1000_0000) != 0;
141 position_comment.id = Some(ID {
142 address_type,
143 aircraft_type,
144 is_notrack,
145 is_stealth,
146 address,
147 ..Default::default()
148 });
149 } else {
150 unparsed.push(part);
151 }
152 } else if part.len() == 12 && &part[0..2] == "id" && position_comment.id.is_none() {
162 if let (Some(detail), Some(address)) = (
163 u16::from_str_radix(&part[2..6], 16).ok(),
164 u32::from_str_radix(&part[6..12], 16).ok(),
165 ) {
166 let reserved = detail & 0b0000_0000_0000_1111;
167 let address_type = (detail & 0b0000_0011_1111_0000) >> 4;
168 let aircraft_type = ((detail & 0b0011_1100_0000_0000) >> 10) as u8;
169 let is_notrack = (detail & 0b0100_0000_0000_0000) != 0;
170 let is_stealth = (detail & 0b1000_0000_0000_0000) != 0;
171 position_comment.id = Some(ID {
172 reserved: Some(reserved),
173 address_type,
174 aircraft_type,
175 is_notrack,
176 is_stealth,
177 address,
178 });
179 } else {
180 unparsed.push(part);
181 }
182 } else if let Some((value, unit)) = split_value_unit(part) {
183 if unit == "fpm" && position_comment.climb_rate.is_none() {
184 position_comment.climb_rate = value.parse::<i16>().ok();
185 } else if unit == "rot" && position_comment.turn_rate.is_none() {
186 position_comment.turn_rate = value.parse::<f32>().ok();
187 } else if unit == "dB" && position_comment.signal_quality.is_none() {
188 position_comment.signal_quality = value.parse::<f32>().ok();
189 } else if unit == "kHz" && position_comment.frequency_offset.is_none() {
190 position_comment.frequency_offset = value.parse::<f32>().ok();
191 } else if unit == "e" && position_comment.error.is_none() {
192 position_comment.error = value.parse::<u8>().ok();
193 } else if unit == "dBm" && position_comment.signal_power.is_none() {
194 position_comment.signal_power = value.parse::<f32>().ok();
195 } else {
196 unparsed.push(part);
197 }
198 } else if part.len() >= 6
202 && &part[0..3] == "gps"
203 && position_comment.gps_quality.is_none()
204 {
205 if let Some((first, second)) = part[3..].split_once('x') {
206 if first.parse::<u8>().is_ok() && second.parse::<u8>().is_ok() {
207 position_comment.gps_quality = Some(part[3..].to_string());
208 } else {
209 unparsed.push(part);
210 }
211 } else {
212 unparsed.push(part);
213 }
214 } else if part.len() >= 3
217 && &part[0..2] == "FL"
218 && position_comment.flight_level.is_none()
219 {
220 if let Ok(flight_level) = part[2..].parse::<f32>() {
221 position_comment.flight_level = Some(flight_level);
222 } else {
223 unparsed.push(part);
224 }
225 } else if part.len() >= 2
228 && &part[0..1] == "s"
229 && position_comment.software_version.is_none()
230 {
231 if let Ok(software_version) = part[1..].parse::<f32>() {
232 position_comment.software_version = Some(software_version);
233 } else {
234 unparsed.push(part);
235 }
236 } else if part.len() == 3
239 && &part[0..1] == "h"
240 && position_comment.hardware_version.is_none()
241 {
242 if part[1..3].chars().all(|c| c.is_ascii_hexdigit()) {
243 position_comment.hardware_version = u8::from_str_radix(&part[1..3], 16).ok();
244 } else {
245 unparsed.push(part);
246 }
247 } else if part.len() == 7
250 && &part[0..1] == "r"
251 && position_comment.original_address.is_none()
252 {
253 if part[1..7].chars().all(|c| c.is_ascii_hexdigit()) {
254 position_comment.original_address = u32::from_str_radix(&part[1..7], 16).ok();
255 } else {
256 unparsed.push(part);
257 }
258 } else {
259 unparsed.push(part);
260 }
261 }
262 position_comment.unparsed = if !unparsed.is_empty() {
263 Some(unparsed.join(" "))
264 } else {
265 None
266 };
267
268 Ok(position_comment)
269 }
270}
271
272#[test]
273fn test_flr() {
274 let result = "255/045/A=003399 !W03! id06DDFAA3 -613fpm -3.9rot 22.5dB 7e -7.0kHz gps3x7 s7.07 h41 rD002F8".parse::<PositionComment>().unwrap();
275 assert_eq!(
276 result,
277 PositionComment {
278 course: Some(255),
279 speed: Some(45),
280 altitude: Some(3399),
281 additional_precision: Some(AdditionalPrecision { lat: 0, lon: 3 }),
282 id: Some(ID {
283 reserved: None,
284 address_type: 2,
285 aircraft_type: 1,
286 is_stealth: false,
287 is_notrack: false,
288 address: u32::from_str_radix("DDFAA3", 16).unwrap(),
289 }),
290 climb_rate: Some(-613),
291 turn_rate: Some(-3.9),
292 signal_quality: Some(22.5),
293 error: Some(7),
294 frequency_offset: Some(-7.0),
295 gps_quality: Some("3x7".into()),
296 software_version: Some(7.07),
297 hardware_version: Some(65),
298 original_address: u32::from_str_radix("D002F8", 16).ok(),
299 ..Default::default()
300 }
301 );
302}
303
304#[test]
305fn test_trk() {
306 let result =
307 "200/073/A=126433 !W05! id15B50BBB +4237fpm +2.2rot FL1267.81 10.0dB 19e +23.8kHz gps36x55"
308 .parse::<PositionComment>()
309 .unwrap();
310 assert_eq!(
311 result,
312 PositionComment {
313 course: Some(200),
314 speed: Some(73),
315 altitude: Some(126433),
316 additional_precision: Some(AdditionalPrecision { lat: 0, lon: 5 }),
317 id: Some(ID {
318 address_type: 1,
319 aircraft_type: 5,
320 is_stealth: false,
321 is_notrack: false,
322 address: u32::from_str_radix("B50BBB", 16).unwrap(),
323 ..Default::default()
324 }),
325 climb_rate: Some(4237),
326 turn_rate: Some(2.2),
327 signal_quality: Some(10.0),
328 error: Some(19),
329 frequency_offset: Some(23.8),
330 gps_quality: Some("36x55".into()),
331 flight_level: Some(1267.81),
332 signal_power: None,
333 software_version: None,
334 hardware_version: None,
335 original_address: None,
336 unparsed: None
337 }
338 );
339}
340
341#[test]
342fn test_trk2() {
343 let result = "000/000/A=002280 !W59! id07395004 +000fpm +0.0rot FL021.72 40.2dB -15.1kHz gps9x13 +15.8dBm".parse::<PositionComment>().unwrap();
344 assert_eq!(
345 result,
346 PositionComment {
347 course: Some(0),
348 speed: Some(0),
349 altitude: Some(2280),
350 additional_precision: Some(AdditionalPrecision { lat: 5, lon: 9 }),
351 id: Some(ID {
352 address_type: 3,
353 aircraft_type: 1,
354 is_stealth: false,
355 is_notrack: false,
356 address: u32::from_str_radix("395004", 16).unwrap(),
357 ..Default::default()
358 }),
359 climb_rate: Some(0),
360 turn_rate: Some(0.0),
361 signal_quality: Some(40.2),
362 frequency_offset: Some(-15.1),
363 gps_quality: Some("9x13".into()),
364 flight_level: Some(21.72),
365 signal_power: Some(15.8),
366 ..Default::default()
367 }
368 );
369}
370
371#[test]
372fn test_trk2_different_order() {
373 let result = "000/000/A=002280 !W59! -15.1kHz id07395004 +15.8dBm +0.0rot +000fpm FL021.72 40.2dB gps9x13".parse::<PositionComment>().unwrap();
375 assert_eq!(
376 result,
377 PositionComment {
378 course: Some(0),
379 speed: Some(0),
380 altitude: Some(2280),
381 additional_precision: Some(AdditionalPrecision { lat: 5, lon: 9 }),
382 id: Some(ID {
383 address_type: 3,
384 aircraft_type: 1,
385 is_stealth: false,
386 is_notrack: false,
387 address: u32::from_str_radix("395004", 16).unwrap(),
388 ..Default::default()
389 }),
390 climb_rate: Some(0),
391 turn_rate: Some(0.0),
392 signal_quality: Some(40.2),
393 frequency_offset: Some(-15.1),
394 gps_quality: Some("9x13".into()),
395 flight_level: Some(21.72),
396 signal_power: Some(15.8),
397 ..Default::default()
398 }
399 );
400}
401
402#[test]
403fn test_bad_gps() {
404 let result = "208/063/A=003222 !W97! id06D017DC -395fpm -2.4rot 8.2dB -6.1kHz gps2xFLRD0"
405 .parse::<PositionComment>()
406 .unwrap();
407 assert_eq!(result.frequency_offset, Some(-6.1));
408 assert_eq!(result.gps_quality.is_some(), false);
409 assert_eq!(result.unparsed, Some("gps2xFLRD0".to_string()));
410}
411
412#[test]
413fn test_naviter_id() {
414 let result = "000/000/A=000000 !W0! id985F579BDF"
415 .parse::<PositionComment>()
416 .unwrap();
417 assert_eq!(result.id.is_some(), true);
418 let id = result.id.unwrap();
419
420 assert_eq!(id.reserved, Some(15));
421 assert_eq!(id.address_type, 5);
422 assert_eq!(id.aircraft_type, 6);
423 assert_eq!(id.is_stealth, true);
424 assert_eq!(id.is_notrack, false);
425 assert_eq!(id.address, 0x579BDF);
426}