ogn_parser/
position_comment.rs

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            // The first part can be course + speed + altitude: ccc/sss/A=aaaaaa
84            // ccc: course in degrees 0-360
85            // sss: speed in km/h
86            // aaaaaa: altitude in feet
87            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            // ... or just the altitude: /A=aaaaaa
108            // aaaaaa: altitude in feet
109            } 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            // ... or a complete weather report
119            // the first character defines the type of the data, the "d" is for the digits
120            //
121            // mandatory fields:
122            // gddd: gust (peak wind speed in mph in the last 5 minutes)
123            // tddd: temperature (in degrees Fahrenheit). Temperatures below zero are expressed as -01 to -99
124            //
125            // optional fields:
126            // rddd: rainfall (in hundrets of inches) in the last hour
127            // pddd: rainfall (in hundrets of inches) in the last 24 hours
128            // Pddd: rainfall (in hundrets of inches) since midnight
129            // hdd: humidity (in % where 00 is 100%)
130            // bddddd: barometric pressure (in tenths of millibars/tenths of hPascal)
131            } 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            // The second part can be the additional precision: !Wab!
166            // a: additional latitude precision
167            // b: additional longitude precision
168            } 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            // generic ID format: idXXYYYYYY (4 bytes format)
186            // YYYYYY: 24 bit address in hex digits
187            // XX in hex digits encodes stealth mode, no-tracking flag and address type
188            // XX to binary-> STtt ttaa
189            // S: stealth flag
190            // T: no-tracking flag
191            // tttt: aircraft type
192            // aa: address type
193            } 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            // NAVITER ID format: idXXXXYYYYYY (5 bytes)
214            // YYYYYY: 24 bit address in hex digits
215            // XXXX in hex digits encodes stealth mode, no-tracking flag and address type
216            // XXXX to binary-> STtt ttaa aaaa rrrr
217            // S: stealth flag
218            // T: no-tracking flag
219            // tttt: aircraft type
220            // aaaaaa: address type
221            // rrrr: (reserved)
222            } 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            // Gps precision: gpsAxB
260            // A: integer
261            // B: integer
262            } 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            // Flight level: FLxx.yy
276            // xx.yy: float value for flight level
277            } 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            // Software version: sXX.YY
287            // XX.YY: float value for software version
288            } 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            // Hardware version: hXX
298            // XX: hexadecimal value for hardware version
299            } 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            // Original address: rXXXXXX
309            // XXXXXX: hex digits for 24 bit address
310            } 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    // Check if order doesn't matter
442    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}