pub mod armor;
pub mod fragments;
pub mod messages;
pub use messages::*;
use armor::decode_armor;
use fragments::FragmentCollector;
use crate::NmeaFrame;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum AisMessage {
Position(PositionReport),
BaseStation(BaseStationReport),
StaticVoyage(StaticVoyageData),
BinaryAddressed(BinaryAddressed),
BinaryAck(BinaryAck),
BinaryBroadcast(BinaryBroadcast),
SarAircraft(SarAircraftReport),
UtcDateResponse(UtcDateResponse),
SafetyAddressed(SafetyAddressed),
Safety(SafetyBroadcast),
Interrogation(Interrogation),
AidToNavigation(AidToNavigation),
StaticReport(StaticDataReport),
LongRangePosition(LongRangePosition),
Unknown { msg_type: u8 },
}
pub struct AisParser {
collector: FragmentCollector,
}
impl AisParser {
pub fn new() -> Self {
Self {
collector: FragmentCollector::new(),
}
}
pub fn reset(&mut self) {
self.collector = FragmentCollector::new();
}
pub fn decode(&mut self, frame: &NmeaFrame<'_>) -> Option<AisMessage> {
if frame.prefix != '!' || (frame.sentence_type != "VDM" && frame.sentence_type != "VDO") {
return None;
}
let payload = self.collector.process(&frame.fields)?;
let bits = decode_armor(&payload.payload, payload.fill_bits)?;
let msg_type = armor::extract_u32(&bits, 0, 6)? as u8;
match msg_type {
1..=3 => PositionReport::decode_class_a(&bits).map(AisMessage::Position),
4 => BaseStationReport::decode(&bits).map(AisMessage::BaseStation),
5 => StaticVoyageData::decode(&bits).map(AisMessage::StaticVoyage),
6 => BinaryAddressed::decode(&bits).map(AisMessage::BinaryAddressed),
7 | 13 => BinaryAck::decode(&bits).map(AisMessage::BinaryAck),
8 => BinaryBroadcast::decode(&bits).map(AisMessage::BinaryBroadcast),
9 => SarAircraftReport::decode(&bits).map(AisMessage::SarAircraft),
11 => UtcDateResponse::decode(&bits).map(AisMessage::UtcDateResponse),
12 => SafetyAddressed::decode(&bits).map(AisMessage::SafetyAddressed),
14 => SafetyBroadcast::decode(&bits).map(AisMessage::Safety),
15 => Interrogation::decode(&bits).map(AisMessage::Interrogation),
18 => PositionReport::decode_class_b(&bits).map(AisMessage::Position),
19 => PositionReport::decode_class_b_extended(&bits).map(AisMessage::Position),
21 => AidToNavigation::decode(&bits).map(AisMessage::AidToNavigation),
24 => StaticDataReport::decode(&bits).map(AisMessage::StaticReport),
27 => LongRangePosition::decode(&bits).map(AisMessage::LongRangePosition),
_ => Some(AisMessage::Unknown { msg_type }),
}
}
}
impl Default for AisParser {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_frame;
#[test]
fn ignores_nmea_sentences() {
let mut parser = AisParser::new();
let frame =
parse_frame("$GPRMC,175957.917,A,3857.1234,N,07705.1234,W,0.0,0.0,010100,,,A*77")
.expect("valid");
assert!(parser.decode(&frame).is_none());
}
#[test]
fn sentinel_values_filtered() {
let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
let msg = parser.decode(&frame).expect("decoded");
if let AisMessage::Position(pos) = msg {
assert!(pos.heading.is_none() || pos.heading.expect("heading") < 360);
}
}
#[test]
fn type_18_class_b() {
let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,B6CdCm0t3`tba35f@V9faHi7kP06,0*58").expect("valid");
let msg = parser.decode(&frame);
if let Some(AisMessage::Position(pos)) = &msg {
assert_eq!(pos.ais_class, AisClass::B);
}
}
#[test]
fn type_19_class_b_extended() {
let mut parser = AisParser::new();
let frame =
parse_frame("!AIVDM,1,1,,B,C5N3SRgPEnJGEBT>NhWAwwo862PaLELTBJ:V00000000S0D:R220,0*0B")
.expect("valid type 19 frame");
let msg = parser.decode(&frame).expect("decode type 19");
if let AisMessage::Position(pos) = msg {
assert_eq!(pos.msg_type, 19);
assert!(pos.mmsi > 0);
assert!(pos.latitude.is_some());
assert!(pos.longitude.is_some());
assert_eq!(pos.ais_class, AisClass::BPlus);
} else {
panic!("expected Position (type 19), got {msg:?}");
}
}
#[test]
fn type_1_position_report() {
let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").expect("valid");
let msg = parser.decode(&frame).expect("decoded");
if let AisMessage::Position(pos) = msg {
assert_eq!(pos.msg_type, 1);
assert!(pos.mmsi > 0);
assert!(pos.latitude.is_some());
assert!(pos.longitude.is_some());
assert_eq!(pos.ais_class, AisClass::A);
let lat = pos.latitude.expect("valid");
let lon = pos.longitude.expect("valid");
assert!((-90.0..=90.0).contains(&lat));
assert!((-180.0..=180.0).contains(&lon));
} else {
panic!("expected Position, got {msg:?}");
}
}
#[test]
fn type_24_static_data_report() {
let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,H52N>V@T2rNVPJ2000000000000,2*29")
.expect("valid type 24 frame");
let msg = parser.decode(&frame).expect("decode type 24");
if let AisMessage::StaticReport(report) = msg {
match report {
StaticDataReport::PartA { mmsi, vessel_name } => {
assert!(mmsi > 0);
let _ = vessel_name;
}
StaticDataReport::PartB { mmsi, .. } => {
assert!(mmsi > 0);
}
}
} else {
panic!("expected StaticReport (type 24), got {msg:?}");
}
}
#[test]
fn type_5_multi_fragment() {
let mut parser = AisParser::new();
let f1 = parse_frame(
"!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
)
.expect("valid frag1");
assert!(parser.decode(&f1).is_none());
let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid frag2");
let msg = parser.decode(&f2).expect("decoded");
if let AisMessage::StaticVoyage(svd) = msg {
assert!(svd.mmsi > 0);
assert!(!svd.vessel_name.is_empty());
assert_eq!(svd.ais_class, AisClass::A);
} else {
panic!("expected StaticVoyage, got {msg:?}");
}
}
#[test]
fn reset_clears_pending_fragments() {
let mut parser = AisParser::new();
let f1 = parse_frame(
"!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
)
.expect("valid");
assert!(parser.decode(&f1).is_none());
parser.reset();
let f2 = parse_frame("!AIVDM,2,2,1,A,88888888880,2*25").expect("valid");
assert!(parser.decode(&f2).is_none());
}
#[test]
fn type_8_binary_broadcast() {
let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,85Mv070j2d>=<e<<=PQhhg`59P00,0*26").expect("valid");
let msg = parser.decode(&frame);
if let Some(AisMessage::BinaryBroadcast(bb)) = msg {
assert!(bb.mmsi > 0);
} else {
panic!("expected BinaryBroadcast type 8, got {msg:?}");
}
}
#[test]
fn type_14_safety_broadcast() {
let mut parser = AisParser::new();
let frame =
parse_frame("!AIVDM,1,1,,A,>5?Per18=HB1U:1@E=B0m<L,0*53").expect("valid type 14 frame");
let msg = parser.decode(&frame).expect("decoded");
if let AisMessage::Safety(broadcast) = msg {
assert!(broadcast.mmsi > 0, "MMSI must be set");
} else {
panic!("expected Safety (type 14), got {msg:?}");
}
}
#[test]
fn type_14_empty_text_no_panic() {
let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,>5?Per1,0*64").expect("valid minimal type 14");
let _ = parser.decode(&frame);
}
#[test]
fn type_21_aid_to_navigation() {
let mut parser = AisParser::new();
let frame =
parse_frame("!AIVDM,1,1,,B,E>jCfrv2`0c2h0W:0a0h6220d5Du0`Htp00000l1@Dc2P0,4*3C")
.expect("valid type 21 frame");
let msg = parser.decode(&frame).expect("decoded");
if let AisMessage::AidToNavigation(aton) = msg {
assert!(aton.mmsi > 0, "MMSI must be set");
assert!(
aton.aid_type <= 31,
"aid_type must be 0–31, got {}",
aton.aid_type
);
} else {
panic!("expected AidToNavigation (type 21), got {msg:?}");
}
}
#[test]
fn type_21_position_in_range() {
let mut parser = AisParser::new();
let frame =
parse_frame("!AIVDM,1,1,,B,E>jCfrv2`0c2h0W:0a0h6220d5Du0`Htp00000l1@Dc2P0,4*3C")
.expect("valid type 21");
let msg = parser.decode(&frame).expect("decoded");
if let AisMessage::AidToNavigation(aton) = msg {
if let (Some(lat), Some(lon)) = (aton.lat, aton.lon) {
assert!((-90.0..=90.0).contains(&lat), "lat out of range: {lat}");
assert!((-180.0..=180.0).contains(&lon), "lon out of range: {lon}");
}
}
}
}