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