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