ogn_parser/
position_comment.rs

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            // The first part can be course + speed + altitude: ccc/sss/A=aaaaaa
89            // ccc: course in degrees 0-360
90            // sss: speed in km/h
91            // aaaaaa: altitude in feet
92            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            // ... or just the altitude: /A=aaaaaa
113            // aaaaaa: altitude in feet
114            } 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            // ... or a complete weather report: ccc/sss/XXX...
124            // starting ccc/sss is now wind_direction and wind_speed
125            // XXX... is a string of data pairs, where each pair has one letter that indicates the type of data and a number that indicates the value
126            //
127            // mandatory fields:
128            // gddd: gust (peak wind speed in mph in the last 5 minutes)
129            // tddd: temperature (in degrees Fahrenheit). Temperatures below zero are expressed as -01 to -99
130            //
131            // optional fields:
132            // rddd: rainfall (in hundrets of inches) in the last hour
133            // pddd: rainfall (in hundrets of inches) in the last 24 hours
134            // Pddd: rainfall (in hundrets of inches) since midnight
135            // hdd: humidity (in % where 00 is 100%)
136            // bddddd: barometric pressure (in tenths of millibars/tenths of hPascal)
137            } 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                // check if any type of data is not in the allowed set or if any type is duplicated
156                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            // The second part can be the additional precision: !Wab!
178            // a: additional latitude precision
179            // b: additional longitude precision
180            } 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            // generic ID format: idXXYYYYYY (4 bytes format)
198            // YYYYYY: 24 bit address in hex digits
199            // XX in hex digits encodes stealth mode, no-tracking flag and address type
200            // XX to binary-> STtt ttaa
201            // S: stealth flag
202            // T: no-tracking flag
203            // tttt: aircraft type
204            // aa: address type
205            } 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            // NAVITER ID format: idXXXXYYYYYY (5 bytes)
226            // YYYYYY: 24 bit address in hex digits
227            // XXXX in hex digits encodes stealth mode, no-tracking flag and address type
228            // XXXX to binary-> STtt ttaa aaaa rrrr
229            // S: stealth flag
230            // T: no-tracking flag
231            // tttt: aircraft type
232            // aaaaaa: address type
233            // rrrr: (reserved)
234            } 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            // Gps precision: gpsAxB
276            // A: integer
277            // B: integer
278            } 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            // Flight level: FLxx.yy
292            // xx.yy: float value for flight level
293            } 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            // Software version: sXX.YY
303            // XX.YY: float value for software version
304            } 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            // Hardware version: hXX
314            // XX: hexadecimal value for hardware version
315            } 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            // Original address: rXXXXXX
325            // XXXXXX: hex digits for 24 bit address
326            } 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    // Check if order doesn't matter
460    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}