wte_mt_rx_parser/
mt_structured.rs

1//! MT(1) Raw Data Serial Out Packet Format
2//!
3//! Data provided from the MT-RX via serial ports/TCP is in the following format:
4//! - `MT1UUUNNNTFHHHHHHHHHHHHHHHSS112233N4445566WYYYY`
5//!
6//! Where:
7//! - `MT1` is fixed and actually “MT1”
8//! - `UUU` - is a 3 character MT-RX configurable ID – by default this is “001”
9//! - `NNN` -is a 3 decimal digit cycling packet sequence number from 000 to 511. This sequence number
10//! increments after each new test or distress message is received. After 511 the sequence cycles to 000
11//! and begins again.
12//! - `T` – is a single character message type 'T' or 'A' (test or distress alert)
13//! - `F` – is a single character format flag 'S' or 'L' (short or long) – this relates to the 406 beacon
14//! transmission specification.
15//! - `HHHHHHHHHHHHHHH` – is a 15 character hex code used to define beacon owner and beacon
16//! capabilities as per the 406 beacon specification.
17//! - `SS` – is a 2 character signal strength indication – “00” if not used.
18//! - `11` – is a 2 decimal character latitude degrees
19//! - `22` – is a 2 decimal character latitude minutes
20//! - `33` – is a 2 decimal character latitude seconds
21//! - `N` – is 'N' or 'S'
22//! - `444` – is a 3 decimal character longitude degrees
23//! - `22` – is a 2 decimal character longitude minutes
24//! - `55` – is a 2 decimal character longitude seconds
25//! - `W–` is 'W' or 'E'
26//! - `YYYY` – is a 4 character checksum (calculated from M – the first character)
27//! - If all location characters are '-' then there is no location information available.
28//!
29//! Legitimate example packet:
30//! `MT1001000AL400C592753572B323433212S1723756E4706`
31
32use crate::ParseError;
33
34/// Represents a cardinal direction.
35#[derive(Clone, Debug, PartialEq)]
36pub enum CardinalDirection {
37    North,
38    South,
39    West,
40    East,
41    Unknown,
42}
43
44impl std::convert::From<char> for CardinalDirection {
45    fn from(c: char) -> Self {
46        match c {
47            'W' => CardinalDirection::West,
48            'E' => CardinalDirection::East,
49            'N' => CardinalDirection::North,
50            'S' => CardinalDirection::South,
51            _ => CardinalDirection::Unknown,
52        }
53    }
54}
55
56/// Represents a MT message type.
57#[derive(Clone, Debug, PartialEq)]
58pub enum MtMessageType {
59    Test,
60    Alert,
61    Unknown,
62}
63
64impl std::convert::From<char> for MtMessageType {
65    fn from(c: char) -> Self {
66        match c {
67            'T' => MtMessageType::Test,
68            'A' => MtMessageType::Alert,
69            _ => MtMessageType::Unknown,
70        }
71    }
72}
73
74/// MT Serial Out Packet Format.
75#[derive(Clone, Debug, PartialEq)]
76pub struct MtStructured {
77    /// `MT1`.
78    pub header: String,
79
80    /// MT-RX configurable ID.
81    pub id: String,
82
83    /// Cycling packet sequence number.
84    pub sequence_number: usize,
85
86    /// Message type.
87    pub message_type: MtMessageType,
88
89    /// Character format flag.
90    pub format_flag: char,
91
92    /// Beacon hex code.
93    pub beacon: String,
94
95    /// Signal strength indication.
96    pub signal_strength: String,
97
98    /// Latitude degrees. `None` if not available.
99    pub lat_degrees: Option<u8>,
100
101    /// Latitude minutes. `None` if not available.
102    pub lat_minutes: Option<u8>,
103
104    /// Latitude seconds. `None` if not available.
105    pub lat_seconds: Option<u8>,
106
107    /// North or South.
108    pub lat_direction: CardinalDirection,
109
110    /// Longitude degrees. `None` if not available.
111    pub long_degrees: Option<u16>,
112
113    /// Longitude minutes. `None` if not available.
114    pub long_minutes: Option<u8>,
115
116    /// Longitude seconds. `None` if not available.
117    pub long_seconds: Option<u8>,
118
119    /// West or East.
120    pub long_direction: CardinalDirection,
121
122    /// Checksum.
123    /// If there is no location information available, hence the value will be `0`.
124    pub checksum: u16,
125}
126
127/// Returns whether `message` is a valid MT(1) message.
128///
129/// ## Examples
130/// ```
131/// use wte_mt_rx_parser::mt_structured;
132/// println!("is it MT1? {}", mt_structured::is_mt("MT1001000AL400C592753572B323433212S1723756E4706"));
133/// ```
134pub fn is_mt(message: &str) -> bool {
135    return message.starts_with("MT1");
136}
137
138/// Tries to parse a "MT Serial Out Packet Format" `message`.
139///
140/// ## Notes
141/// - Checksum is not calculated here.
142///
143/// ## Examples
144/// ```
145/// use wte_mt_rx_parser::mt_structured;
146/// let parsed = mt_structured::parse("MT1001000AL400C592753572B323433212S1723756E4706").unwrap();
147/// // ...
148/// ```
149///
150/// ## Message format
151/// Data provided should be in the following format:
152/// - `MT1UUUNNNTFHHHHHHHHHHHHHHHSS112233N4445566WYYYY`
153pub fn parse(message: &str) -> Result<MtStructured, ParseError> {
154    // 012 345 678 9 0 123456789012345 67 89 01 23 4 567 89 01 2 3456
155    // MT1 UUU NNN T F HHHHHHHHHHHHHHH SS 11 22 33 N 444 55 66 W YYYY
156
157    const MT1_LEN: usize = 47;
158    if message.len() != MT1_LEN {
159        return Err(ParseError::SizeNotMatch {
160            expected: MT1_LEN,
161            found: message.len(),
162        });
163    }
164
165    let header = message[0..3].to_string();
166    let id = message[3..6].to_string();
167    let sequence_number = message[6..9].parse::<usize>()?;
168    let message_type = (message.as_bytes()[9] as char).into();
169    let format_flag = message.as_bytes()[10] as char;
170    let beacon = message[11..26].to_string();
171    let signal_strength = message[26..28].to_string();
172    let lat_degrees = message[28..30].parse::<u8>().ok();
173    let lat_minutes = message[30..32].parse::<u8>().ok();
174    let lat_seconds = message[32..34].parse::<u8>().ok();
175    let lat_direction = (message.as_bytes()[34] as char).into();
176    let long_degrees = message[35..38].parse::<u16>().ok();
177    let long_minutes = message[38..40].parse::<u8>().ok();
178    let long_seconds = message[40..42].parse::<u8>().ok();
179    let long_direction = (message.as_bytes()[42] as char).into();
180    let checksum = u16::from_str_radix(&message[43..47], 16).unwrap_or(0);
181
182    // TODO: calculate checksum here?
183
184    let result = MtStructured {
185        header,
186        id,
187        sequence_number,
188        message_type,
189        format_flag,
190        beacon,
191        signal_strength,
192        lat_degrees,
193        lat_minutes,
194        lat_seconds,
195        lat_direction,
196        long_degrees,
197        long_minutes,
198        long_seconds,
199        long_direction,
200        checksum,
201    };
202
203    Ok(result)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn fields() {
212        // MT1 UUU NNN T F HHHHHHHHHHHHHHH SS 11 22 33 N 444 55 66 W YYYY
213
214        // MT1 001 000 A L 400C592753572B3 23 43 32 12 S 172 37 56 E 4706 <- valid
215        let parsed = parse("MT1001000AL400C592753572B323433212S1723756E4706").unwrap();
216        assert_eq!(parsed.id, "001");
217        assert_eq!(parsed.sequence_number, 0);
218        assert_eq!(parsed.message_type, MtMessageType::Alert);
219        assert_eq!(parsed.format_flag, 'L');
220        assert_eq!(parsed.beacon, "400C592753572B3");
221        assert_eq!(parsed.signal_strength, "23");
222    }
223
224    #[test]
225    fn valid_location_and_direction() {
226        // MT1 UUU NNN T F HHHHHHHHHHHHHHH SS 11 22 33 N 444 55 66 W YYYY
227
228        // MT1 001 000 A L 400C592753572B3 23 43 32 12 S 172 37 56 E 4706 <- valid
229        let parsed = parse("MT1001000AL400C592753572B323433212S1723756E4706").unwrap();
230
231        assert_eq!(parsed.lat_degrees.unwrap(), 43);
232        assert_eq!(parsed.lat_minutes.unwrap(), 32);
233        assert_eq!(parsed.lat_seconds.unwrap(), 12);
234        assert_eq!(parsed.lat_direction, CardinalDirection::South);
235
236        assert_eq!(parsed.long_degrees.unwrap(), 172);
237        assert_eq!(parsed.long_minutes.unwrap(), 37);
238        assert_eq!(parsed.long_seconds.unwrap(), 56);
239        assert_eq!(parsed.long_direction, CardinalDirection::East);
240    }
241
242    #[test]
243    fn unknown_location() {
244        // MT1 UUU NNN T F HHHHHHHHHHHHHHH SS 11 22 33 N 444 55 66 W YYYY
245
246        // MT1 001 000 A L 400C592753572B3 23 43 32 12 S --- -- -- E 4706 <- invalid
247        let parsed = parse("MT1001000AL400C592753572B323433212S-------E4706").unwrap();
248        assert!(parsed.long_degrees.is_none());
249        assert!(parsed.long_minutes.is_none());
250        assert!(parsed.long_seconds.is_none());
251
252        // MT1 001 000 A L 400C592753572B3 23 -- -- -- S 172 37 56 E 4706 <- invalid
253        let parsed = parse("MT1001000AL400C592753572B323------S1723756E4706").unwrap();
254        assert!(parsed.lat_degrees.is_none());
255        assert!(parsed.lat_minutes.is_none());
256        assert!(parsed.lat_seconds.is_none());
257    }
258}