ogn_parser/
position_comment.rs

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