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#[derive(Debug, Clone, PartialEq)]
37pub enum AisMessage {
38 Position(PositionReport),
40 StaticVoyage(StaticVoyageData),
42 StaticReport(StaticDataReport),
44 Unknown { msg_type: u8 },
46}
47
48pub struct AisParser {
53 collector: FragmentCollector,
54}
55
56impl AisParser {
57 pub fn new() -> Self {
58 Self {
59 collector: FragmentCollector::new(),
60 }
61 }
62
63 pub fn reset(&mut self) {
67 self.collector = FragmentCollector::new();
68 }
69
70 pub fn decode(&mut self, frame: &NmeaFrame<'_>) -> Option<AisMessage> {
73 if frame.prefix != '!' || (frame.sentence_type != "VDM" && frame.sentence_type != "VDO") {
75 return None;
76 }
77
78 let payload = self.collector.process(&frame.fields)?;
80
81 let bits = decode_armor(&payload.payload, payload.fill_bits)?;
83
84 let msg_type = armor::extract_u32(&bits, 0, 6)? as u8;
86
87 match msg_type {
89 1..=3 => PositionReport::decode_class_a(&bits).map(AisMessage::Position),
90 5 => StaticVoyageData::decode(&bits).map(AisMessage::StaticVoyage),
91 18 => PositionReport::decode_class_b(&bits).map(AisMessage::Position),
92 19 => PositionReport::decode_class_b_extended(&bits).map(AisMessage::Position),
93 24 => StaticDataReport::decode(&bits).map(AisMessage::StaticReport),
94 _ => Some(AisMessage::Unknown { msg_type }),
95 }
96 }
97}
98
99impl Default for AisParser {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::parse_frame;
109
110 #[test]
111 fn ignores_nmea_sentences() {
112 let mut parser = AisParser::new();
113 let frame =
114 parse_frame("$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*77")
115 .expect("valid");
116 assert!(parser.decode(&frame).is_none());
117 }
118
119 #[test]
120 fn sentinel_values_filtered() {
121 let mut parser = AisParser::new();
122 let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
123 let msg = parser.decode(&frame).expect("decoded");
124 if let AisMessage::Position(pos) = msg {
125 assert!(pos.heading.is_none() || pos.heading.expect("heading") < 360);
126 }
127 }
128
129 #[test]
130 fn type_18_class_b() {
131 let mut parser = AisParser::new();
132 let frame = parse_frame("!AIVDM,1,1,,A,B6CdCm0t3`tba35f@V9faHi7kP06,0*58").expect("valid");
133 let msg = parser.decode(&frame);
134 if let Some(AisMessage::Position(pos)) = &msg {
137 assert_eq!(pos.ais_class, AisClass::B);
138 }
139 }
140
141 #[test]
142 fn type_19_class_b_extended() {
143 let mut parser = AisParser::new();
144 let frame =
146 parse_frame("!AIVDM,1,1,,B,C5N3SRgPEnJGEBT>NhWAwwo862PaLELTBJ:V00000000S0D:R220,0*0B")
147 .expect("valid type 19 frame");
148 let msg = parser.decode(&frame).expect("decode type 19");
149 if let AisMessage::Position(pos) = msg {
150 assert_eq!(pos.msg_type, 19);
151 assert!(pos.mmsi > 0);
152 assert!(pos.latitude.is_some());
153 assert!(pos.longitude.is_some());
154 assert_eq!(pos.ais_class, AisClass::BPlus);
155 } else {
156 panic!("expected Position (type 19), got {msg:?}");
157 }
158 }
159
160 #[test]
161 fn type_1_position_report() {
162 let mut parser = AisParser::new();
163 let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
164 let msg = parser.decode(&frame).expect("decoded");
165 if let AisMessage::Position(pos) = msg {
166 assert_eq!(pos.msg_type, 1);
167 assert!(pos.mmsi > 0);
168 assert!(pos.latitude.is_some());
169 assert!(pos.longitude.is_some());
170 assert_eq!(pos.ais_class, AisClass::A);
171 let lat = pos.latitude.expect("valid");
173 let lon = pos.longitude.expect("valid");
174 assert!((-90.0..=90.0).contains(&lat));
175 assert!((-180.0..=180.0).contains(&lon));
176 } else {
177 panic!("expected Position, got {msg:?}");
178 }
179 }
180
181 #[test]
182 fn type_24_static_data_report() {
183 let mut parser = AisParser::new();
184 let frame = parse_frame("!AIVDM,1,1,,A,H52N>V@T2rNVPJ2000000000000,2*29")
186 .expect("valid type 24 frame");
187 let msg = parser.decode(&frame).expect("decode type 24");
188 if let AisMessage::StaticReport(report) = msg {
189 match report {
190 StaticDataReport::PartA { mmsi, vessel_name } => {
191 assert!(mmsi > 0);
192 let _ = vessel_name;
194 }
195 StaticDataReport::PartB { mmsi, .. } => {
196 assert!(mmsi > 0);
197 }
198 }
199 } else {
200 panic!("expected StaticReport (type 24), got {msg:?}");
201 }
202 }
203
204 #[test]
205 fn type_5_multi_fragment() {
206 let mut parser = AisParser::new();
207
208 let f1 = parse_frame(
210 "!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
211 )
212 .expect("valid frag1");
213 assert!(parser.decode(&f1).is_none()); let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid frag2");
216 let msg = parser.decode(&f2).expect("decoded");
217 if let AisMessage::StaticVoyage(svd) = msg {
218 assert!(svd.mmsi > 0);
219 assert!(!svd.vessel_name.is_empty());
220 assert_eq!(svd.ais_class, AisClass::A);
221 } else {
222 panic!("expected StaticVoyage, got {msg:?}");
223 }
224 }
225
226 #[test]
227 fn reset_clears_pending_fragments() {
228 let mut parser = AisParser::new();
229 let f1 = parse_frame(
231 "!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
232 )
233 .expect("valid");
234 assert!(parser.decode(&f1).is_none());
235 parser.reset();
237 let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid");
239 assert!(parser.decode(&f2).is_none());
240 }
241
242 #[test]
243 fn unknown_message_type() {
244 let mut parser = AisParser::new();
245 let frame = parse_frame("!AIVDM,1,1,,A,85Mv070j2d>=<e<<=PQhhg`59P00,0*26").expect("valid");
247 let msg = parser.decode(&frame);
248 if let Some(AisMessage::Unknown { msg_type }) = msg {
249 assert_eq!(msg_type, 8);
250 } else {
251 panic!("expected Unknown type 8, got {msg:?}");
252 }
253 }
254}