1use serde::Serialize;
2use std::{convert::Infallible, str::FromStr};
3
4use crate::utils::{split_letter_number_pairs, 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_if = "Option::is_none")]
31 pub wind_direction: Option<u16>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub wind_speed: Option<u16>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub gust: Option<u16>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub temperature: Option<i16>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub rainfall_1h: Option<u16>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub rainfall_24h: Option<u16>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub rainfall_midnight: Option<u16>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub humidity: Option<u8>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub barometric_pressure: Option<u32>,
48 #[serde(skip_serializing)]
49 pub additional_precision: Option<AdditionalPrecision>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 #[serde(flatten)]
52 pub id: Option<ID>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub climb_rate: Option<i16>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub turn_rate: Option<f32>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub signal_quality: Option<f32>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub error: Option<u8>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub frequency_offset: Option<f32>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub gps_quality: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub flight_level: Option<f32>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub signal_power: Option<f32>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub software_version: Option<f32>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub hardware_version: Option<u8>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub original_address: Option<u32>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub unparsed: Option<String>,
77}
78
79impl FromStr for PositionComment {
80 type Err = Infallible;
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 let mut position_comment = PositionComment {
83 ..Default::default()
84 };
85 let mut unparsed: Vec<_> = vec![];
86 for (idx, part) in s.split_ascii_whitespace().enumerate() {
87 if idx == 0
92 && part.len() == 16
93 && &part[3..4] == "/"
94 && &part[7..10] == "/A="
95 && position_comment.course.is_none()
96 {
97 let course = part[0..3].parse::<u16>().ok();
98 let speed = part[4..7].parse::<u16>().ok();
99 let altitude = part[10..16].parse::<u32>().ok();
100 if course.is_some()
101 && course.unwrap() <= 360
102 && speed.is_some()
103 && altitude.is_some()
104 {
105 position_comment.course = course;
106 position_comment.speed = speed;
107 position_comment.altitude = altitude;
108 } else {
109 unparsed.push(part);
110 }
111 } else if idx == 0
114 && part.len() == 9
115 && &part[0..3] == "/A="
116 && position_comment.altitude.is_none()
117 {
118 match part[3..].parse::<u32>().ok() {
119 Some(altitude) => position_comment.altitude = Some(altitude),
120 None => unparsed.push(part),
121 }
122 } else if idx == 0
137 && part.len() >= 15
138 && &part[3..4] == "/"
139 && position_comment.wind_direction.is_none()
140 {
141 let wind_direction = part[0..3].parse::<u16>().ok();
142 let wind_speed = part[4..7].parse::<u16>().ok();
143
144 if wind_direction.is_some() && wind_speed.is_some() {
145 position_comment.wind_direction = wind_direction;
146 position_comment.wind_speed = wind_speed;
147 } else {
148 unparsed.push(part);
149 continue;
150 }
151
152 let pairs = split_letter_number_pairs(&part[7..]);
153
154 let mut seen = std::collections::HashSet::new();
156 if pairs
157 .iter()
158 .any(|(c, _)| !seen.insert(*c) || !"gtrpPhb".contains(*c))
159 {
160 unparsed.push(part);
161 continue;
162 }
163
164 for (c, number) in pairs {
165 match c {
166 'g' => position_comment.gust = Some(number as u16),
167 't' => position_comment.temperature = Some(number as i16),
168 'r' => position_comment.rainfall_1h = Some(number as u16),
169 'p' => position_comment.rainfall_24h = Some(number as u16),
170 'P' => position_comment.rainfall_midnight = Some(number as u16),
171 'h' => position_comment.humidity = Some(number as u8),
172 'b' => position_comment.barometric_pressure = Some(number as u32),
173 _ => unreachable!(),
174 }
175 }
176 } else if idx == 1
180 && part.len() == 5
181 && &part[0..2] == "!W"
182 && &part[4..] == "!"
183 && position_comment.additional_precision.is_none()
184 {
185 let add_lat = part[2..3].parse::<u8>().ok();
186 let add_lon = part[3..4].parse::<u8>().ok();
187 match (add_lat, add_lon) {
188 (Some(add_lat), Some(add_lon)) => {
189 position_comment.additional_precision = Some(AdditionalPrecision {
190 lat: add_lat,
191 lon: add_lon,
192 })
193 }
194 _ => unparsed.push(part),
195 }
196 } else if part.len() == 10 && &part[0..2] == "id" && position_comment.id.is_none() {
205 if let (Some(detail), Some(address)) = (
206 u8::from_str_radix(&part[2..4], 16).ok(),
207 u32::from_str_radix(&part[4..10], 16).ok(),
208 ) {
209 let address_type = (detail & 0b0000_0011) as u16;
210 let aircraft_type = (detail & 0b_0011_1100) >> 2;
211 let is_notrack = (detail & 0b0100_0000) != 0;
212 let is_stealth = (detail & 0b1000_0000) != 0;
213 position_comment.id = Some(ID {
214 address_type,
215 aircraft_type,
216 is_notrack,
217 is_stealth,
218 address,
219 ..Default::default()
220 });
221 } else {
222 unparsed.push(part);
223 }
224 } else if part.len() == 12 && &part[0..2] == "id" && position_comment.id.is_none() {
234 if let (Some(detail), Some(address)) = (
235 u16::from_str_radix(&part[2..6], 16).ok(),
236 u32::from_str_radix(&part[6..12], 16).ok(),
237 ) {
238 let reserved = detail & 0b0000_0000_0000_1111;
239 let address_type = (detail & 0b0000_0011_1111_0000) >> 4;
240 let aircraft_type = ((detail & 0b0011_1100_0000_0000) >> 10) as u8;
241 let is_notrack = (detail & 0b0100_0000_0000_0000) != 0;
242 let is_stealth = (detail & 0b1000_0000_0000_0000) != 0;
243 position_comment.id = Some(ID {
244 reserved: Some(reserved),
245 address_type,
246 aircraft_type,
247 is_notrack,
248 is_stealth,
249 address,
250 });
251 } else {
252 unparsed.push(part);
253 }
254 } else if let Some((value, unit)) = split_value_unit(part) {
255 if unit == "fpm" && position_comment.climb_rate.is_none() {
256 position_comment.climb_rate = value.parse::<i16>().ok();
257 } else if unit == "rot" && position_comment.turn_rate.is_none() {
258 position_comment.turn_rate = value.parse::<f32>().ok();
259 } else if unit == "dB" && position_comment.signal_quality.is_none() {
260 position_comment.signal_quality = value.parse::<f32>().ok();
261 } else if unit == "kHz" && position_comment.frequency_offset.is_none() {
262 position_comment.frequency_offset = value.parse::<f32>().ok();
263 } else if unit == "e" && position_comment.error.is_none() {
264 position_comment.error = value.parse::<u8>().ok();
265 } else if unit == "dBm" && position_comment.signal_power.is_none() {
266 position_comment.signal_power = value.parse::<f32>().ok();
267 } else {
268 unparsed.push(part);
269 }
270 } else if part.len() >= 6
274 && &part[0..3] == "gps"
275 && position_comment.gps_quality.is_none()
276 {
277 if let Some((first, second)) = part[3..].split_once('x') {
278 if first.parse::<u8>().is_ok() && second.parse::<u8>().is_ok() {
279 position_comment.gps_quality = Some(part[3..].to_string());
280 } else {
281 unparsed.push(part);
282 }
283 } else {
284 unparsed.push(part);
285 }
286 } else if part.len() >= 3
289 && &part[0..2] == "FL"
290 && position_comment.flight_level.is_none()
291 {
292 if let Ok(flight_level) = part[2..].parse::<f32>() {
293 position_comment.flight_level = Some(flight_level);
294 } else {
295 unparsed.push(part);
296 }
297 } else if part.len() >= 2
300 && &part[0..1] == "s"
301 && position_comment.software_version.is_none()
302 {
303 if let Ok(software_version) = part[1..].parse::<f32>() {
304 position_comment.software_version = Some(software_version);
305 } else {
306 unparsed.push(part);
307 }
308 } else if part.len() == 3
311 && &part[0..1] == "h"
312 && position_comment.hardware_version.is_none()
313 {
314 if part[1..3].chars().all(|c| c.is_ascii_hexdigit()) {
315 position_comment.hardware_version = u8::from_str_radix(&part[1..3], 16).ok();
316 } else {
317 unparsed.push(part);
318 }
319 } else if part.len() == 7
322 && &part[0..1] == "r"
323 && position_comment.original_address.is_none()
324 {
325 if part[1..7].chars().all(|c| c.is_ascii_hexdigit()) {
326 position_comment.original_address = u32::from_str_radix(&part[1..7], 16).ok();
327 } else {
328 unparsed.push(part);
329 }
330 } else {
331 unparsed.push(part);
332 }
333 }
334 position_comment.unparsed = if !unparsed.is_empty() {
335 Some(unparsed.join(" "))
336 } else {
337 None
338 };
339
340 Ok(position_comment)
341 }
342}
343
344#[test]
345fn test_flr() {
346 let result = "255/045/A=003399 !W03! id06DDFAA3 -613fpm -3.9rot 22.5dB 7e -7.0kHz gps3x7 s7.07 h41 rD002F8".parse::<PositionComment>().unwrap();
347 assert_eq!(
348 result,
349 PositionComment {
350 course: Some(255),
351 speed: Some(45),
352 altitude: Some(3399),
353 additional_precision: Some(AdditionalPrecision { lat: 0, lon: 3 }),
354 id: Some(ID {
355 reserved: None,
356 address_type: 2,
357 aircraft_type: 1,
358 is_stealth: false,
359 is_notrack: false,
360 address: u32::from_str_radix("DDFAA3", 16).unwrap(),
361 }),
362 climb_rate: Some(-613),
363 turn_rate: Some(-3.9),
364 signal_quality: Some(22.5),
365 error: Some(7),
366 frequency_offset: Some(-7.0),
367 gps_quality: Some("3x7".into()),
368 software_version: Some(7.07),
369 hardware_version: Some(65),
370 original_address: u32::from_str_radix("D002F8", 16).ok(),
371 ..Default::default()
372 }
373 );
374}
375
376#[test]
377fn test_trk() {
378 let result =
379 "200/073/A=126433 !W05! id15B50BBB +4237fpm +2.2rot FL1267.81 10.0dB 19e +23.8kHz gps36x55"
380 .parse::<PositionComment>()
381 .unwrap();
382 assert_eq!(
383 result,
384 PositionComment {
385 course: Some(200),
386 speed: Some(73),
387 altitude: Some(126433),
388 wind_direction: None,
389 wind_speed: None,
390 gust: None,
391 temperature: None,
392 rainfall_1h: None,
393 rainfall_24h: None,
394 rainfall_midnight: None,
395 humidity: None,
396 barometric_pressure: None,
397 additional_precision: Some(AdditionalPrecision { lat: 0, lon: 5 }),
398 id: Some(ID {
399 address_type: 1,
400 aircraft_type: 5,
401 is_stealth: false,
402 is_notrack: false,
403 address: u32::from_str_radix("B50BBB", 16).unwrap(),
404 ..Default::default()
405 }),
406 climb_rate: Some(4237),
407 turn_rate: Some(2.2),
408 signal_quality: Some(10.0),
409 error: Some(19),
410 frequency_offset: Some(23.8),
411 gps_quality: Some("36x55".into()),
412 flight_level: Some(1267.81),
413 signal_power: None,
414 software_version: None,
415 hardware_version: None,
416 original_address: None,
417 unparsed: None
418 }
419 );
420}
421
422#[test]
423fn test_trk2() {
424 let result = "000/000/A=002280 !W59! id07395004 +000fpm +0.0rot FL021.72 40.2dB -15.1kHz gps9x13 +15.8dBm".parse::<PositionComment>().unwrap();
425 assert_eq!(
426 result,
427 PositionComment {
428 course: Some(0),
429 speed: Some(0),
430 altitude: Some(2280),
431 additional_precision: Some(AdditionalPrecision { lat: 5, lon: 9 }),
432 id: Some(ID {
433 address_type: 3,
434 aircraft_type: 1,
435 is_stealth: false,
436 is_notrack: false,
437 address: u32::from_str_radix("395004", 16).unwrap(),
438 ..Default::default()
439 }),
440 climb_rate: Some(0),
441 turn_rate: Some(0.0),
442 signal_quality: Some(40.2),
443 frequency_offset: Some(-15.1),
444 gps_quality: Some("9x13".into()),
445 flight_level: Some(21.72),
446 signal_power: Some(15.8),
447 ..Default::default()
448 }
449 );
450}
451
452#[test]
453fn test_trk2_different_order() {
454 let result = "000/000/A=002280 !W59! -15.1kHz id07395004 +15.8dBm +0.0rot +000fpm FL021.72 40.2dB gps9x13".parse::<PositionComment>().unwrap();
456 assert_eq!(
457 result,
458 PositionComment {
459 course: Some(0),
460 speed: Some(0),
461 altitude: Some(2280),
462 additional_precision: Some(AdditionalPrecision { lat: 5, lon: 9 }),
463 id: Some(ID {
464 address_type: 3,
465 aircraft_type: 1,
466 is_stealth: false,
467 is_notrack: false,
468 address: u32::from_str_radix("395004", 16).unwrap(),
469 ..Default::default()
470 }),
471 climb_rate: Some(0),
472 turn_rate: Some(0.0),
473 signal_quality: Some(40.2),
474 frequency_offset: Some(-15.1),
475 gps_quality: Some("9x13".into()),
476 flight_level: Some(21.72),
477 signal_power: Some(15.8),
478 ..Default::default()
479 }
480 );
481}
482
483#[test]
484fn test_bad_gps() {
485 let result = "208/063/A=003222 !W97! id06D017DC -395fpm -2.4rot 8.2dB -6.1kHz gps2xFLRD0"
486 .parse::<PositionComment>()
487 .unwrap();
488 assert_eq!(result.frequency_offset, Some(-6.1));
489 assert_eq!(result.gps_quality.is_some(), false);
490 assert_eq!(result.unparsed, Some("gps2xFLRD0".to_string()));
491}
492
493#[test]
494fn test_naviter_id() {
495 let result = "000/000/A=000000 !W0! id985F579BDF"
496 .parse::<PositionComment>()
497 .unwrap();
498 assert_eq!(result.id.is_some(), true);
499 let id = result.id.unwrap();
500
501 assert_eq!(id.reserved, Some(15));
502 assert_eq!(id.address_type, 5);
503 assert_eq!(id.aircraft_type, 6);
504 assert_eq!(id.is_stealth, true);
505 assert_eq!(id.is_notrack, false);
506 assert_eq!(id.address, 0x579BDF);
507}
508
509#[test]
510fn parse_weather() {
511 let result = "187/004g007t075h78b63620"
512 .parse::<PositionComment>()
513 .unwrap();
514 assert_eq!(result.wind_direction, Some(187));
515 assert_eq!(result.wind_speed, Some(4));
516 assert_eq!(result.gust, Some(7));
517 assert_eq!(result.temperature, Some(75));
518 assert_eq!(result.humidity, Some(78));
519 assert_eq!(result.barometric_pressure, Some(63620));
520}
521
522#[test]
523fn parse_weather_bad_type() {
524 let result = "187/004g007X075h78b63620"
525 .parse::<PositionComment>()
526 .unwrap();
527 assert_eq!(
528 result.unparsed,
529 Some("187/004g007X075h78b63620".to_string())
530 );
531}
532
533#[test]
534fn parse_weather_duplicate_type() {
535 let result = "187/004g007t075g78b63620"
536 .parse::<PositionComment>()
537 .unwrap();
538 assert_eq!(
539 result.unparsed,
540 Some("187/004g007t075g78b63620".to_string())
541 );
542}