1#[cfg(feature = "ais")]
29pub mod armor;
30#[cfg(feature = "ais")]
31pub mod fragments;
32#[cfg(feature = "ais")]
33pub mod messages;
34#[cfg(any(feature = "abm", feature = "bbm", feature = "vsd"))]
35pub mod sentences;
36
37#[cfg(feature = "ais")]
38pub use messages::*;
39
40#[cfg(feature = "ais")]
41use armor::decode_armor;
42#[cfg(feature = "ais")]
43use fragments::FragmentCollector;
44
45#[cfg(feature = "ais")]
46use crate::NmeaFrame;
47
48#[cfg(feature = "ais")]
50#[non_exhaustive]
51#[derive(Debug, Clone, PartialEq)]
52pub enum AisMessage {
53 Position(PositionReport),
55 BaseStation(BaseStationReport),
57 StaticVoyage(StaticVoyageData),
59 BinaryAddressed(BinaryAddressed),
61 BinaryAck(BinaryAck),
63 BinaryBroadcast(BinaryBroadcast),
65 SarAircraft(SarAircraftReport),
67 UtcDateResponse(UtcDateResponse),
69 SafetyAddressed(SafetyAddressed),
71 Safety(SafetyBroadcast),
73 Interrogation(Interrogation),
75 AidToNavigation(AidToNavigation),
77 StaticReport(StaticDataReport),
79 LongRangePosition(LongRangePosition),
81 Unknown { msg_type: u8 },
83}
84
85#[cfg(feature = "ais")]
90pub struct AisParser {
91 collector: FragmentCollector,
92}
93
94#[cfg(feature = "ais")]
95impl AisParser {
96 pub fn new() -> Self {
97 Self {
98 collector: FragmentCollector::new(),
99 }
100 }
101
102 pub fn reset(&mut self) {
106 self.collector = FragmentCollector::new();
107 }
108
109 pub fn decode(&mut self, frame: &NmeaFrame<'_>) -> Option<AisMessage> {
112 if frame.prefix != '!' || (frame.sentence_type != "VDM" && frame.sentence_type != "VDO") {
114 return None;
115 }
116
117 let payload = self.collector.process(&frame.fields)?;
119
120 let bits = decode_armor(&payload.payload, payload.fill_bits)?;
122
123 let msg_type = armor::extract_u32(&bits, 0, 6)? as u8;
125
126 match msg_type {
128 1..=3 => PositionReport::decode_class_a(&bits).map(AisMessage::Position),
129 4 => BaseStationReport::decode(&bits).map(AisMessage::BaseStation),
130 5 => StaticVoyageData::decode(&bits).map(AisMessage::StaticVoyage),
131 6 => BinaryAddressed::decode(&bits).map(AisMessage::BinaryAddressed),
132 7 | 13 => BinaryAck::decode(&bits).map(AisMessage::BinaryAck),
133 8 => BinaryBroadcast::decode(&bits).map(AisMessage::BinaryBroadcast),
134 9 => SarAircraftReport::decode(&bits).map(AisMessage::SarAircraft),
135 11 => UtcDateResponse::decode(&bits).map(AisMessage::UtcDateResponse),
136 12 => SafetyAddressed::decode(&bits).map(AisMessage::SafetyAddressed),
137 14 => SafetyBroadcast::decode(&bits).map(AisMessage::Safety),
138 15 => Interrogation::decode(&bits).map(AisMessage::Interrogation),
139 18 => PositionReport::decode_class_b(&bits).map(AisMessage::Position),
140 19 => PositionReport::decode_class_b_extended(&bits).map(AisMessage::Position),
141 21 => AidToNavigation::decode(&bits).map(AisMessage::AidToNavigation),
142 24 => StaticDataReport::decode(&bits).map(AisMessage::StaticReport),
143 27 => LongRangePosition::decode(&bits).map(AisMessage::LongRangePosition),
144 _ => Some(AisMessage::Unknown { msg_type }),
145 }
146 }
147}
148
149#[cfg(feature = "ais")]
150impl Default for AisParser {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[cfg(test)]
157#[cfg(feature = "ais")]
158mod tests {
159 use super::*;
160 use crate::parse_frame;
161
162 #[test]
163 fn ignores_nmea_sentences() {
164 let mut parser = AisParser::new();
165 let frame =
166 parse_frame("$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*77")
167 .expect("valid");
168 assert!(parser.decode(&frame).is_none());
169 }
170
171 #[test]
172 fn sentinel_values_filtered() {
173 let mut parser = AisParser::new();
174 let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
175 let msg = parser.decode(&frame).expect("decoded");
176 if let AisMessage::Position(pos) = msg {
177 assert!(pos.heading.is_none() || pos.heading.expect("heading") < 360);
178 }
179 }
180
181 #[test]
182 fn type_18_class_b() {
183 let mut parser = AisParser::new();
184 let frame = parse_frame("!AIVDM,1,1,,A,B6CdCm0t3`tba35f@V9faHi7kP06,0*58").expect("valid");
185 let msg = parser.decode(&frame);
186 if let Some(AisMessage::Position(pos)) = &msg {
189 assert_eq!(pos.ais_class, AisClass::B);
190 }
191 }
192
193 #[test]
194 fn type_19_class_b_extended() {
195 let mut parser = AisParser::new();
196 let frame =
198 parse_frame("!AIVDM,1,1,,B,C5N3SRgPEnJGEBT>NhWAwwo862PaLELTBJ:V00000000S0D:R220,0*0B")
199 .expect("valid type 19 frame");
200 let msg = parser.decode(&frame).expect("decode type 19");
201 if let AisMessage::Position(pos) = msg {
202 assert_eq!(pos.msg_type, 19);
203 assert!(pos.mmsi > 0);
204 assert!(pos.latitude.is_some());
205 assert!(pos.longitude.is_some());
206 assert_eq!(pos.ais_class, AisClass::BPlus);
207 } else {
208 panic!("expected Position (type 19), got {msg:?}");
209 }
210 }
211
212 #[test]
213 fn type_1_position_report() {
214 let mut parser = AisParser::new();
215 let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
216 let msg = parser.decode(&frame).expect("decoded");
217 if let AisMessage::Position(pos) = msg {
218 assert_eq!(pos.msg_type, 1);
219 assert!(pos.mmsi > 0);
220 assert!(pos.latitude.is_some());
221 assert!(pos.longitude.is_some());
222 assert_eq!(pos.ais_class, AisClass::A);
223 let lat = pos.latitude.expect("valid");
225 let lon = pos.longitude.expect("valid");
226 assert!((-90.0..=90.0).contains(&lat));
227 assert!((-180.0..=180.0).contains(&lon));
228 } else {
229 panic!("expected Position, got {msg:?}");
230 }
231 }
232
233 #[test]
234 fn type_24_static_data_report() {
235 let mut parser = AisParser::new();
236 let frame = parse_frame("!AIVDM,1,1,,A,H52N>V@T2rNVPJ2000000000000,2*29")
238 .expect("valid type 24 frame");
239 let msg = parser.decode(&frame).expect("decode type 24");
240 if let AisMessage::StaticReport(report) = msg {
241 match report {
242 StaticDataReport::PartA { mmsi, vessel_name } => {
243 assert!(mmsi > 0);
244 let _ = vessel_name;
246 }
247 StaticDataReport::PartB { mmsi, .. } => {
248 assert!(mmsi > 0);
249 }
250 }
251 } else {
252 panic!("expected StaticReport (type 24), got {msg:?}");
253 }
254 }
255
256 #[test]
257 fn type_5_multi_fragment() {
258 let mut parser = AisParser::new();
259
260 let f1 = parse_frame(
262 "!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
263 )
264 .expect("valid frag1");
265 assert!(parser.decode(&f1).is_none()); let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid frag2");
268 let msg = parser.decode(&f2).expect("decoded");
269 if let AisMessage::StaticVoyage(svd) = msg {
270 assert!(svd.mmsi > 0);
271 assert!(!svd.vessel_name.is_empty());
272 assert_eq!(svd.ais_class, AisClass::A);
273 } else {
274 panic!("expected StaticVoyage, got {msg:?}");
275 }
276 }
277
278 #[test]
279 fn reset_clears_pending_fragments() {
280 let mut parser = AisParser::new();
281 let f1 = parse_frame(
283 "!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
284 )
285 .expect("valid");
286 assert!(parser.decode(&f1).is_none());
287 parser.reset();
289 let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid");
291 assert!(parser.decode(&f2).is_none());
292 }
293
294 #[test]
295 fn type_8_binary_broadcast() {
296 let mut parser = AisParser::new();
297 let frame = parse_frame("!AIVDM,1,1,,A,85Mv070j2d>=<e<<=PQhhg`59P00,0*26").expect("valid");
298 let msg = parser.decode(&frame);
299 if let Some(AisMessage::BinaryBroadcast(bb)) = msg {
300 assert!(bb.mmsi > 0);
301 } else {
302 panic!("expected BinaryBroadcast type 8, got {msg:?}");
303 }
304 }
305
306 #[test]
307 fn type_14_safety_broadcast() {
308 let mut parser = AisParser::new();
309 let frame =
311 parse_frame("!AIVDM,1,1,,A,>5?Per18=HB1U:1@E=B0m<L,0*53").expect("valid type 14 frame");
312 let msg = parser.decode(&frame).expect("decoded");
313 if let AisMessage::Safety(broadcast) = msg {
314 assert!(broadcast.mmsi > 0, "MMSI must be set");
315 } else {
316 panic!("expected Safety (type 14), got {msg:?}");
317 }
318 }
319
320 #[test]
321 fn type_14_empty_text_no_panic() {
322 let mut parser = AisParser::new();
323 let frame = parse_frame("!AIVDM,1,1,,A,>5?Per1,0*64").expect("valid minimal type 14");
325 let _ = parser.decode(&frame);
327 }
328
329 #[test]
330 fn type_21_aid_to_navigation() {
331 let mut parser = AisParser::new();
332 let frame =
335 parse_frame("!AIVDM,1,1,,B,E>jCfrv2`0c2h0W:0a0h6220d5Du0`Htp00000l1@Dc2P0,4*3C")
336 .expect("valid type 21 frame");
337 let msg = parser.decode(&frame).expect("decoded");
338 if let AisMessage::AidToNavigation(aton) = msg {
339 assert!(aton.mmsi > 0, "MMSI must be set");
340 assert!(
341 aton.aid_type <= 31,
342 "aid_type must be 0–31, got {}",
343 aton.aid_type
344 );
345 } else {
346 panic!("expected AidToNavigation (type 21), got {msg:?}");
347 }
348 }
349
350 #[test]
351 fn type_21_position_in_range() {
352 let mut parser = AisParser::new();
353 let frame =
354 parse_frame("!AIVDM,1,1,,B,E>jCfrv2`0c2h0W:0a0h6220d5Du0`Htp00000l1@Dc2P0,4*3C")
355 .expect("valid type 21");
356 let msg = parser.decode(&frame).expect("decoded");
357 if let AisMessage::AidToNavigation(aton) = msg {
358 if let (Some(lat), Some(lon)) = (aton.lat, aton.lon) {
359 assert!((-90.0..=90.0).contains(&lat), "lat out of range: {lat}");
360 assert!((-180.0..=180.0).contains(&lon), "lon out of range: {lon}");
361 }
362 }
363 }
364}