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}