1#[cfg(feature = "ais")]
29pub mod armor;
30#[cfg(feature = "ais")]
31mod encode;
32#[cfg(feature = "ais")]
33pub mod fragments;
34#[cfg(feature = "ais")]
35pub mod messages;
36#[cfg(any(feature = "abm", feature = "bbm"))]
37pub mod sentences;
38#[cfg(feature = "ais")]
39pub mod transmit;
40
41#[cfg(feature = "ais")]
42pub use messages::*;
43
44#[cfg(feature = "ais")]
45use armor::decode_armor;
46#[cfg(feature = "ais")]
47use fragments::FragmentCollector;
48
49#[cfg(feature = "ais")]
50use crate::NmeaFrame;
51
52#[cfg(feature = "ais")]
54#[non_exhaustive]
55#[derive(Debug, Clone, PartialEq)]
56pub enum AisMessage {
57 Position(PositionReport),
59 BaseStation(BaseStationReport),
61 StaticVoyage(StaticVoyageData),
63 BinaryAddressed(BinaryAddressed),
65 BinaryAck(BinaryAck),
67 BinaryBroadcast(BinaryBroadcast),
69 SarAircraft(SarAircraftReport),
71 UtcDateInquiry(UtcDateInquiry),
73 UtcDateResponse(UtcDateResponse),
75 SafetyAddressed(SafetyAddressed),
77 Safety(SafetyBroadcast),
79 Interrogation(Interrogation),
81 AssignmentMode(AssignmentModeCommand),
83 DgnssBroadcast(DgnssBroadcast),
85 AidToNavigation(AidToNavigation),
87 DataLinkManagement(DataLinkManagement),
89 ChannelManagement(ChannelManagement),
91 GroupAssignment(GroupAssignment),
93 StaticReport(StaticDataReport),
95 BinarySingleSlot(BinarySingleSlot),
97 BinaryMultiSlot(BinaryMultiSlot),
99 LongRangePosition(LongRangePosition),
101 Unknown { msg_type: u8 },
103}
104
105#[cfg(feature = "ais")]
110pub struct AisParser {
111 collector: FragmentCollector,
112}
113
114#[cfg(feature = "ais")]
115impl AisParser {
116 pub fn new() -> Self {
117 Self {
118 collector: FragmentCollector::new(),
119 }
120 }
121
122 pub fn reset(&mut self) {
126 self.collector = FragmentCollector::new();
127 }
128
129 pub fn decode(&mut self, frame: &NmeaFrame<'_>) -> Option<AisMessage> {
132 if frame.prefix != '!' || (frame.sentence_type != "VDM" && frame.sentence_type != "VDO") {
134 return None;
135 }
136
137 let payload = self.collector.process(&frame.fields)?;
139
140 let bits = decode_armor(&payload.payload, payload.fill_bits)?;
142
143 let msg_type = armor::extract_u32(&bits, 0, 6)? as u8;
145
146 match msg_type {
148 1..=3 => PositionReport::decode_class_a(&bits).map(AisMessage::Position),
149 4 => BaseStationReport::decode(&bits).map(AisMessage::BaseStation),
150 5 => StaticVoyageData::decode(&bits).map(AisMessage::StaticVoyage),
151 6 => BinaryAddressed::decode(&bits).map(AisMessage::BinaryAddressed),
152 7 | 13 => BinaryAck::decode(&bits).map(AisMessage::BinaryAck),
153 8 => BinaryBroadcast::decode(&bits).map(AisMessage::BinaryBroadcast),
154 9 => SarAircraftReport::decode(&bits).map(AisMessage::SarAircraft),
155 10 => UtcDateInquiry::decode(&bits).map(AisMessage::UtcDateInquiry),
156 11 => UtcDateResponse::decode(&bits).map(AisMessage::UtcDateResponse),
157 12 => SafetyAddressed::decode(&bits).map(AisMessage::SafetyAddressed),
158 14 => SafetyBroadcast::decode(&bits).map(AisMessage::Safety),
159 15 => Interrogation::decode(&bits).map(AisMessage::Interrogation),
160 16 => AssignmentModeCommand::decode(&bits).map(AisMessage::AssignmentMode),
161 17 => DgnssBroadcast::decode(&bits).map(AisMessage::DgnssBroadcast),
162 18 => PositionReport::decode_class_b(&bits).map(AisMessage::Position),
163 19 => PositionReport::decode_class_b_extended(&bits).map(AisMessage::Position),
164 21 => AidToNavigation::decode(&bits).map(AisMessage::AidToNavigation),
165 20 => DataLinkManagement::decode(&bits).map(AisMessage::DataLinkManagement),
166 22 => ChannelManagement::decode(&bits).map(AisMessage::ChannelManagement),
167 23 => GroupAssignment::decode(&bits).map(AisMessage::GroupAssignment),
168 24 => StaticDataReport::decode(&bits).map(AisMessage::StaticReport),
169 25 => BinarySingleSlot::decode(&bits).map(AisMessage::BinarySingleSlot),
170 26 => BinaryMultiSlot::decode(&bits).map(AisMessage::BinaryMultiSlot),
171 27 => LongRangePosition::decode(&bits).map(AisMessage::LongRangePosition),
172 _ => Some(AisMessage::Unknown { msg_type }),
173 }
174 }
175}
176
177#[cfg(feature = "ais")]
178impl Default for AisParser {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184#[cfg(test)]
185#[cfg(feature = "ais")]
186mod tests {
187 use super::*;
188 use crate::ais::armor::encode_armor;
189 use crate::ais::messages::test_helpers::set_bits;
190 use crate::{encode_frame, parse_frame};
191
192 fn decode_bits(bits: &mut [u8]) -> AisMessage {
193 let (payload, fill_bits) = encode_armor(bits);
194 let fill_bits = fill_bits.to_string();
195 let frame = encode_frame('!', "AI", "VDM", &["1", "1", "", "A", &payload, &fill_bits])
196 .expect("frame");
197 AisParser::new()
198 .decode(&parse_frame(&frame).expect("parse"))
199 .expect("decode")
200 }
201
202 #[test]
203 fn dispatches_newly_supported_types() {
204 for (message_type, bit_len) in [
205 (10, 72),
206 (16, 96),
207 (17, 80),
208 (20, 72),
209 (22, 168),
210 (23, 160),
211 (25, 40),
212 (26, 64),
213 ] {
214 let mut bits = vec![0; bit_len];
215 set_bits(&mut bits, 0, 6, message_type);
216 let message = decode_bits(&mut bits);
217 assert!(
218 matches!(
219 (message_type, message),
220 (10, AisMessage::UtcDateInquiry(_))
221 | (16, AisMessage::AssignmentMode(_))
222 | (17, AisMessage::DgnssBroadcast(_))
223 | (20, AisMessage::DataLinkManagement(_))
224 | (22, AisMessage::ChannelManagement(_))
225 | (23, AisMessage::GroupAssignment(_))
226 | (25, AisMessage::BinarySingleSlot(_))
227 | (26, AisMessage::BinaryMultiSlot(_))
228 ),
229 "type {message_type} did not dispatch to its typed variant"
230 );
231 }
232 }
233
234 #[test]
235 fn ignores_nmea_sentences() {
236 let mut parser = AisParser::new();
237 let frame =
238 parse_frame("$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*77")
239 .expect("valid");
240 assert!(parser.decode(&frame).is_none());
241 }
242
243 #[test]
244 fn sentinel_values_filtered() {
245 let mut parser = AisParser::new();
246 let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
247 let msg = parser.decode(&frame).expect("decoded");
248 if let AisMessage::Position(pos) = msg {
249 assert!(pos.heading.is_none() || pos.heading.expect("heading") < 360);
250 }
251 }
252
253 #[test]
254 fn type_18_class_b() {
255 let mut parser = AisParser::new();
256 let frame = parse_frame("!AIVDM,1,1,,A,B6CdCm0t3`tba35f@V9faHi7kP06,0*58").expect("valid");
257 let msg = parser.decode(&frame);
258 if let Some(AisMessage::Position(pos)) = &msg {
261 assert_eq!(pos.ais_class, AisClass::B);
262 }
263 }
264
265 #[test]
266 fn type_19_class_b_extended() {
267 let mut parser = AisParser::new();
268 let frame =
270 parse_frame("!AIVDM,1,1,,B,C5N3SRgPEnJGEBT>NhWAwwo862PaLELTBJ:V00000000S0D:R220,0*0B")
271 .expect("valid type 19 frame");
272 let msg = parser.decode(&frame).expect("decode type 19");
273 if let AisMessage::Position(pos) = msg {
274 assert_eq!(pos.msg_type, 19);
275 assert!(pos.mmsi > 0);
276 assert!(pos.latitude.is_some());
277 assert!(pos.longitude.is_some());
278 assert_eq!(pos.ais_class, AisClass::BPlus);
279 } else {
280 panic!("expected Position (type 19), got {msg:?}");
281 }
282 }
283
284 #[test]
285 fn type_1_position_report() {
286 let mut parser = AisParser::new();
287 let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
288 let msg = parser.decode(&frame).expect("decoded");
289 if let AisMessage::Position(pos) = msg {
290 assert_eq!(pos.msg_type, 1);
291 assert!(pos.mmsi > 0);
292 assert!(pos.latitude.is_some());
293 assert!(pos.longitude.is_some());
294 assert_eq!(pos.ais_class, AisClass::A);
295 let lat = pos.latitude.expect("valid");
297 let lon = pos.longitude.expect("valid");
298 assert!((-90.0..=90.0).contains(&lat));
299 assert!((-180.0..=180.0).contains(&lon));
300 } else {
301 panic!("expected Position, got {msg:?}");
302 }
303 }
304
305 #[test]
306 fn type_24_static_data_report() {
307 let mut parser = AisParser::new();
308 let frame = parse_frame("!AIVDM,1,1,,A,H52N>V@T2rNVPJ2000000000000,2*29")
310 .expect("valid type 24 frame");
311 let msg = parser.decode(&frame).expect("decode type 24");
312 if let AisMessage::StaticReport(report) = msg {
313 match report {
314 StaticDataReport::PartA {
315 mmsi, vessel_name, ..
316 } => {
317 assert!(mmsi > 0);
318 let _ = vessel_name;
320 }
321 StaticDataReport::PartB { mmsi, .. } => {
322 assert!(mmsi > 0);
323 }
324 }
325 } else {
326 panic!("expected StaticReport (type 24), got {msg:?}");
327 }
328 }
329
330 #[test]
331 fn type_5_multi_fragment() {
332 let mut parser = AisParser::new();
333
334 let f1 = parse_frame(
336 "!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
337 )
338 .expect("valid frag1");
339 assert!(parser.decode(&f1).is_none()); let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid frag2");
342 let msg = parser.decode(&f2).expect("decoded");
343 if let AisMessage::StaticVoyage(svd) = msg {
344 assert!(svd.mmsi > 0);
345 assert!(!svd.vessel_name.is_empty());
346 assert_eq!(svd.ais_class, AisClass::A);
347 } else {
348 panic!("expected StaticVoyage, got {msg:?}");
349 }
350 }
351
352 #[test]
353 fn reset_clears_pending_fragments() {
354 let mut parser = AisParser::new();
355 let f1 = parse_frame(
357 "!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
358 )
359 .expect("valid");
360 assert!(parser.decode(&f1).is_none());
361 parser.reset();
363 let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid");
365 assert!(parser.decode(&f2).is_none());
366 }
367
368 #[test]
369 fn type_8_binary_broadcast() {
370 let mut parser = AisParser::new();
371 let frame = parse_frame("!AIVDM,1,1,,A,85Mv070j2d>=<e<<=PQhhg`59P00,0*26").expect("valid");
372 let msg = parser.decode(&frame);
373 if let Some(AisMessage::BinaryBroadcast(bb)) = msg {
374 assert!(bb.mmsi > 0);
375 } else {
376 panic!("expected BinaryBroadcast type 8, got {msg:?}");
377 }
378 }
379
380 #[test]
381 fn type_14_safety_broadcast() {
382 let mut parser = AisParser::new();
383 let frame =
385 parse_frame("!AIVDM,1,1,,A,>5?Per18=HB1U:1@E=B0m<L,0*53").expect("valid type 14 frame");
386 let msg = parser.decode(&frame).expect("decoded");
387 if let AisMessage::Safety(broadcast) = msg {
388 assert!(broadcast.mmsi > 0, "MMSI must be set");
389 } else {
390 panic!("expected Safety (type 14), got {msg:?}");
391 }
392 }
393
394 #[test]
395 fn type_14_empty_text_no_panic() {
396 let mut parser = AisParser::new();
397 let frame = parse_frame("!AIVDM,1,1,,A,>5?Per1,0*64").expect("valid minimal type 14");
399 let _ = parser.decode(&frame);
401 }
402
403 #[test]
404 fn type_21_aid_to_navigation() {
405 let mut parser = AisParser::new();
406 let frame =
409 parse_frame("!AIVDM,1,1,,B,E>jCfrv2`0c2h0W:0a0h6220d5Du0`Htp00000l1@Dc2P0,4*3C")
410 .expect("valid type 21 frame");
411 let msg = parser.decode(&frame).expect("decoded");
412 if let AisMessage::AidToNavigation(aton) = msg {
413 assert!(aton.mmsi > 0, "MMSI must be set");
414 assert!(
415 aton.aid_type <= 31,
416 "aid_type must be 0–31, got {}",
417 aton.aid_type
418 );
419 } else {
420 panic!("expected AidToNavigation (type 21), got {msg:?}");
421 }
422 }
423
424 #[test]
425 fn type_21_position_in_range() {
426 let mut parser = AisParser::new();
427 let frame =
428 parse_frame("!AIVDM,1,1,,B,E>jCfrv2`0c2h0W:0a0h6220d5Du0`Htp00000l1@Dc2P0,4*3C")
429 .expect("valid type 21");
430 let msg = parser.decode(&frame).expect("decoded");
431 if let AisMessage::AidToNavigation(aton) = msg {
432 if let (Some(lat), Some(lon)) = (aton.lat, aton.lon) {
433 assert!((-90.0..=90.0).contains(&lat), "lat out of range: {lat}");
434 assert!((-180.0..=180.0).contains(&lon), "lon out of range: {lon}");
435 }
436 }
437 }
438}