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