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