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