ogn_parser/
position_comment.rs

1use serde::Serialize;
2use std::{convert::Infallible, str::FromStr};
3
4use crate::utils::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)]
31    pub additional_precision: Option<AdditionalPrecision>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    #[serde(flatten)]
34    pub id: Option<ID>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub climb_rate: Option<i16>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub turn_rate: Option<f32>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub signal_quality: Option<f32>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub error: Option<u8>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub frequency_offset: Option<f32>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub gps_quality: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub flight_level: Option<f32>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub signal_power: Option<f32>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub software_version: Option<f32>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub hardware_version: Option<u8>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub original_address: Option<u32>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub unparsed: Option<String>,
59}
60
61impl FromStr for PositionComment {
62    type Err = Infallible;
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        let mut position_comment = PositionComment {
65            ..Default::default()
66        };
67        let mut unparsed: Vec<_> = vec![];
68        for (idx, part) in s.split_ascii_whitespace().enumerate() {
69            // The first part can be course + speed + altitude: ccc/sss/A=aaaaaa
70            // ccc: course in degrees 0-360
71            // sss: speed in km/h
72            // aaaaaa: altitude in feet
73            if idx == 0 && part.len() == 16 && position_comment.course.is_none() {
74                let subparts = part.split('/').collect::<Vec<_>>();
75                let course = subparts[0].parse::<u16>().ok();
76                let speed = subparts[1].parse::<u16>().ok();
77                let altitude = if &subparts[2][0..2] == "A=" {
78                    subparts[2][2..].parse::<u32>().ok()
79                } else {
80                    None
81                };
82                if course.is_some()
83                    && course.unwrap() <= 360
84                    && speed.is_some()
85                    && altitude.is_some()
86                {
87                    position_comment.course = course;
88                    position_comment.speed = speed;
89                    position_comment.altitude = altitude;
90                } else {
91                    unparsed.push(part);
92                }
93            // ... or just the altitude: /A=aaaaaa
94            // aaaaaa: altitude in feet
95            } else if idx == 0
96                && part.len() == 9
97                && &part[0..3] == "/A="
98                && position_comment.altitude.is_none()
99            {
100                match part[3..].parse::<u32>().ok() {
101                    Some(altitude) => position_comment.altitude = Some(altitude),
102                    None => unparsed.push(part),
103                }
104            // The second part can be the additional precision: !Wab!
105            // a: additional latitude precision
106            // b: additional longitude precision
107            } else if idx == 1
108                && part.len() == 5
109                && &part[0..2] == "!W"
110                && &part[4..] == "!"
111                && position_comment.additional_precision.is_none()
112            {
113                let add_lat = part[2..3].parse::<u8>().ok();
114                let add_lon = part[3..4].parse::<u8>().ok();
115                match (add_lat, add_lon) {
116                    (Some(add_lat), Some(add_lon)) => {
117                        position_comment.additional_precision = Some(AdditionalPrecision {
118                            lat: add_lat,
119                            lon: add_lon,
120                        })
121                    }
122                    _ => unparsed.push(part),
123                }
124            // generic ID format: idXXYYYYYY (4 bytes format)
125            // YYYYYY: 24 bit address in hex digits
126            // XX in hex digits encodes stealth mode, no-tracking flag and address type
127            // XX to binary-> STtt ttaa
128            // S: stealth flag
129            // T: no-tracking flag
130            // tttt: aircraft type
131            // aa: address type
132            } else if part.len() == 10 && &part[0..2] == "id" && position_comment.id.is_none() {
133                if let (Some(detail), Some(address)) = (
134                    u8::from_str_radix(&part[2..4], 16).ok(),
135                    u32::from_str_radix(&part[4..10], 16).ok(),
136                ) {
137                    let address_type = (detail & 0b0000_0011) as u16;
138                    let aircraft_type = (detail & 0b_0011_1100) >> 2;
139                    let is_notrack = (detail & 0b0100_0000) != 0;
140                    let is_stealth = (detail & 0b1000_0000) != 0;
141                    position_comment.id = Some(ID {
142                        address_type,
143                        aircraft_type,
144                        is_notrack,
145                        is_stealth,
146                        address,
147                        ..Default::default()
148                    });
149                } else {
150                    unparsed.push(part);
151                }
152            // NAVITER ID format: idXXXXYYYYYY (5 bytes)
153            // YYYYYY: 24 bit address in hex digits
154            // XXXX in hex digits encodes stealth mode, no-tracking flag and address type
155            // XXXX to binary-> STtt ttaa aaaa rrrr
156            // S: stealth flag
157            // T: no-tracking flag
158            // tttt: aircraft type
159            // aaaaaa: address type
160            // rrrr: (reserved)
161            } else if part.len() == 12 && &part[0..2] == "id" && position_comment.id.is_none() {
162                if let (Some(detail), Some(address)) = (
163                    u16::from_str_radix(&part[2..6], 16).ok(),
164                    u32::from_str_radix(&part[6..12], 16).ok(),
165                ) {
166                    let reserved = detail & 0b0000_0000_0000_1111;
167                    let address_type = (detail & 0b0000_0011_1111_0000) >> 4;
168                    let aircraft_type = ((detail & 0b0011_1100_0000_0000) >> 10) as u8;
169                    let is_notrack = (detail & 0b0100_0000_0000_0000) != 0;
170                    let is_stealth = (detail & 0b1000_0000_0000_0000) != 0;
171                    position_comment.id = Some(ID {
172                        reserved: Some(reserved),
173                        address_type,
174                        aircraft_type,
175                        is_notrack,
176                        is_stealth,
177                        address,
178                    });
179                } else {
180                    unparsed.push(part);
181                }
182            } else if let Some((value, unit)) = split_value_unit(part) {
183                if unit == "fpm" && position_comment.climb_rate.is_none() {
184                    position_comment.climb_rate = value.parse::<i16>().ok();
185                } else if unit == "rot" && position_comment.turn_rate.is_none() {
186                    position_comment.turn_rate = value.parse::<f32>().ok();
187                } else if unit == "dB" && position_comment.signal_quality.is_none() {
188                    position_comment.signal_quality = value.parse::<f32>().ok();
189                } else if unit == "kHz" && position_comment.frequency_offset.is_none() {
190                    position_comment.frequency_offset = value.parse::<f32>().ok();
191                } else if unit == "e" && position_comment.error.is_none() {
192                    position_comment.error = value.parse::<u8>().ok();
193                } else if unit == "dBm" && position_comment.signal_power.is_none() {
194                    position_comment.signal_power = value.parse::<f32>().ok();
195                } else {
196                    unparsed.push(part);
197                }
198            // Gps precision: gpsAxB
199            // A: integer
200            // B: integer
201            } else if part.len() >= 6
202                && &part[0..3] == "gps"
203                && position_comment.gps_quality.is_none()
204            {
205                if let Some((first, second)) = part[3..].split_once('x') {
206                    if first.parse::<u8>().is_ok() && second.parse::<u8>().is_ok() {
207                        position_comment.gps_quality = Some(part[3..].to_string());
208                    } else {
209                        unparsed.push(part);
210                    }
211                } else {
212                    unparsed.push(part);
213                }
214            // Flight level: FLxx.yy
215            // xx.yy: float value for flight level
216            } else if part.len() >= 3
217                && &part[0..2] == "FL"
218                && position_comment.flight_level.is_none()
219            {
220                if let Ok(flight_level) = part[2..].parse::<f32>() {
221                    position_comment.flight_level = Some(flight_level);
222                } else {
223                    unparsed.push(part);
224                }
225            // Software version: sXX.YY
226            // XX.YY: float value for software version
227            } else if part.len() >= 2
228                && &part[0..1] == "s"
229                && position_comment.software_version.is_none()
230            {
231                if let Ok(software_version) = part[1..].parse::<f32>() {
232                    position_comment.software_version = Some(software_version);
233                } else {
234                    unparsed.push(part);
235                }
236            // Hardware version: hXX
237            // XX: hexadecimal value for hardware version
238            } else if part.len() == 3
239                && &part[0..1] == "h"
240                && position_comment.hardware_version.is_none()
241            {
242                if part[1..3].chars().all(|c| c.is_ascii_hexdigit()) {
243                    position_comment.hardware_version = u8::from_str_radix(&part[1..3], 16).ok();
244                } else {
245                    unparsed.push(part);
246                }
247            // Original address: rXXXXXX
248            // XXXXXX: hex digits for 24 bit address
249            } else if part.len() == 7
250                && &part[0..1] == "r"
251                && position_comment.original_address.is_none()
252            {
253                if part[1..7].chars().all(|c| c.is_ascii_hexdigit()) {
254                    position_comment.original_address = u32::from_str_radix(&part[1..7], 16).ok();
255                } else {
256                    unparsed.push(part);
257                }
258            } else {
259                unparsed.push(part);
260            }
261        }
262        position_comment.unparsed = if !unparsed.is_empty() {
263            Some(unparsed.join(" "))
264        } else {
265            None
266        };
267
268        Ok(position_comment)
269    }
270}
271
272#[test]
273fn test_flr() {
274    let result = "255/045/A=003399 !W03! id06DDFAA3 -613fpm -3.9rot 22.5dB 7e -7.0kHz gps3x7 s7.07 h41 rD002F8".parse::<PositionComment>().unwrap();
275    assert_eq!(
276        result,
277        PositionComment {
278            course: Some(255),
279            speed: Some(45),
280            altitude: Some(3399),
281            additional_precision: Some(AdditionalPrecision { lat: 0, lon: 3 }),
282            id: Some(ID {
283                reserved: None,
284                address_type: 2,
285                aircraft_type: 1,
286                is_stealth: false,
287                is_notrack: false,
288                address: u32::from_str_radix("DDFAA3", 16).unwrap(),
289            }),
290            climb_rate: Some(-613),
291            turn_rate: Some(-3.9),
292            signal_quality: Some(22.5),
293            error: Some(7),
294            frequency_offset: Some(-7.0),
295            gps_quality: Some("3x7".into()),
296            software_version: Some(7.07),
297            hardware_version: Some(65),
298            original_address: u32::from_str_radix("D002F8", 16).ok(),
299            ..Default::default()
300        }
301    );
302}
303
304#[test]
305fn test_trk() {
306    let result =
307        "200/073/A=126433 !W05! id15B50BBB +4237fpm +2.2rot FL1267.81 10.0dB 19e +23.8kHz gps36x55"
308            .parse::<PositionComment>()
309            .unwrap();
310    assert_eq!(
311        result,
312        PositionComment {
313            course: Some(200),
314            speed: Some(73),
315            altitude: Some(126433),
316            additional_precision: Some(AdditionalPrecision { lat: 0, lon: 5 }),
317            id: Some(ID {
318                address_type: 1,
319                aircraft_type: 5,
320                is_stealth: false,
321                is_notrack: false,
322                address: u32::from_str_radix("B50BBB", 16).unwrap(),
323                ..Default::default()
324            }),
325            climb_rate: Some(4237),
326            turn_rate: Some(2.2),
327            signal_quality: Some(10.0),
328            error: Some(19),
329            frequency_offset: Some(23.8),
330            gps_quality: Some("36x55".into()),
331            flight_level: Some(1267.81),
332            signal_power: None,
333            software_version: None,
334            hardware_version: None,
335            original_address: None,
336            unparsed: None
337        }
338    );
339}
340
341#[test]
342fn test_trk2() {
343    let result = "000/000/A=002280 !W59! id07395004 +000fpm +0.0rot FL021.72 40.2dB -15.1kHz gps9x13 +15.8dBm".parse::<PositionComment>().unwrap();
344    assert_eq!(
345        result,
346        PositionComment {
347            course: Some(0),
348            speed: Some(0),
349            altitude: Some(2280),
350            additional_precision: Some(AdditionalPrecision { lat: 5, lon: 9 }),
351            id: Some(ID {
352                address_type: 3,
353                aircraft_type: 1,
354                is_stealth: false,
355                is_notrack: false,
356                address: u32::from_str_radix("395004", 16).unwrap(),
357                ..Default::default()
358            }),
359            climb_rate: Some(0),
360            turn_rate: Some(0.0),
361            signal_quality: Some(40.2),
362            frequency_offset: Some(-15.1),
363            gps_quality: Some("9x13".into()),
364            flight_level: Some(21.72),
365            signal_power: Some(15.8),
366            ..Default::default()
367        }
368    );
369}
370
371#[test]
372fn test_trk2_different_order() {
373    // Check if order doesn't matter
374    let result = "000/000/A=002280 !W59! -15.1kHz id07395004 +15.8dBm +0.0rot +000fpm FL021.72 40.2dB gps9x13".parse::<PositionComment>().unwrap();
375    assert_eq!(
376        result,
377        PositionComment {
378            course: Some(0),
379            speed: Some(0),
380            altitude: Some(2280),
381            additional_precision: Some(AdditionalPrecision { lat: 5, lon: 9 }),
382            id: Some(ID {
383                address_type: 3,
384                aircraft_type: 1,
385                is_stealth: false,
386                is_notrack: false,
387                address: u32::from_str_radix("395004", 16).unwrap(),
388                ..Default::default()
389            }),
390            climb_rate: Some(0),
391            turn_rate: Some(0.0),
392            signal_quality: Some(40.2),
393            frequency_offset: Some(-15.1),
394            gps_quality: Some("9x13".into()),
395            flight_level: Some(21.72),
396            signal_power: Some(15.8),
397            ..Default::default()
398        }
399    );
400}
401
402#[test]
403fn test_bad_gps() {
404    let result = "208/063/A=003222 !W97! id06D017DC -395fpm -2.4rot 8.2dB -6.1kHz gps2xFLRD0"
405        .parse::<PositionComment>()
406        .unwrap();
407    assert_eq!(result.frequency_offset, Some(-6.1));
408    assert_eq!(result.gps_quality.is_some(), false);
409    assert_eq!(result.unparsed, Some("gps2xFLRD0".to_string()));
410}
411
412#[test]
413fn test_naviter_id() {
414    let result = "000/000/A=000000 !W0! id985F579BDF"
415        .parse::<PositionComment>()
416        .unwrap();
417    assert_eq!(result.id.is_some(), true);
418    let id = result.id.unwrap();
419
420    assert_eq!(id.reserved, Some(15));
421    assert_eq!(id.address_type, 5);
422    assert_eq!(id.aircraft_type, 6);
423    assert_eq!(id.is_stealth, true);
424    assert_eq!(id.is_notrack, false);
425    assert_eq!(id.address, 0x579BDF);
426}