diff --git a/src/messages/addressed_safety_related.rs b/src/messages/addressed_safety_related.rs new file mode 100644 index 0000000..188de6e --- /dev/null +++ b/src/messages/addressed_safety_related.rs @@ -0,0 +1,100 @@ +//! Addressed Safety-Related Message (type 12) +use super::parsers::*; +use super::AisMessageType; +use crate::errors::Result; +use nom::bits::{bits, complete::take as take_bits}; +use nom::combinator::map; +use nom::IResult; + +#[derive(Debug, PartialEq)] +pub struct AddressedSafetyRelatedMessage { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub seqno: u8, + pub dest_mmsi: u32, + pub retransmit: bool, + pub text: AsciiString, +} + +impl<'a> AisMessageType<'a> for AddressedSafetyRelatedMessage { + fn name(&self) -> &'static str { + "Addressed Safety-Related Message" + } + + fn parse(data: &'a [u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} + +fn parse_base(data: &[u8]) -> IResult<&[u8], AddressedSafetyRelatedMessage> { + bits(move |data| -> IResult<_, _> { + let (data, message_type) = take_bits(6u8)(data)?; + let (data, repeat_indicator) = take_bits(2u8)(data)?; + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, seqno) = take_bits(2u8)(data)?; + let (data, dest_mmsi) = take_bits(30u32)(data)?; + let (data, retransmit) = map(take_bits(1u8), u8_to_bool)(data)?; + let (data, _spare) = take_bits::<_, u8, _, _>(1u8)(data)?; + + // Ensure there are enough bits remaining for text + let remaining_bits = remaining_bits(data); + if remaining_bits < 6 { + return Err(nom::Err::Error(nom::error::Error::new( + data, + nom::error::ErrorKind::Eof, + ))); + } + + let (data, text) = parse_6bit_ascii(data, remaining_bits)?; + + Ok(( + data, + AddressedSafetyRelatedMessage { + message_type, + repeat_indicator, + mmsi, + seqno, + dest_mmsi, + retransmit, + text, + }, + )) + })(data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_type12_example_a() { + let bytestream = b"<5?SIj1;GbD07??4"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = AddressedSafetyRelatedMessage::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 12); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 351853000); + assert_eq!(report.seqno, 0); + assert_eq!(report.dest_mmsi, 316123456); + assert_eq!(report.retransmit, false); + assert_eq!(report.text, "GOOD"); + } + + #[test] + fn test_type12_example_b() { + let bytestream = b"<42Lati0W:Ov=C7P6B?=Pjoihhjhqq0"; + let bitstream = crate::messages::unarmor(bytestream, 2).unwrap(); + let report = AddressedSafetyRelatedMessage::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 12); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 271002099); + assert_eq!(report.seqno, 0); + assert_eq!(report.dest_mmsi, 271002111); + assert_eq!(report.retransmit, true); + assert_eq!(report.text, "MSG FROM 271002099"); + } +} diff --git a/src/messages/binary_addressed.rs b/src/messages/binary_addressed.rs new file mode 100644 index 0000000..03da4aa --- /dev/null +++ b/src/messages/binary_addressed.rs @@ -0,0 +1,124 @@ +//! Binary Addressed Message (type 6) +use super::parsers::u8_to_bool; +use super::AisMessageType; +use crate::errors::Result; +use crate::lib; +use nom::bits::{bits, complete::take as take_bits}; +use nom::combinator::map; +use nom::IResult; + +#[cfg(all(not(feature = "std"), not(feature = "alloc")))] +const MAX_DATA_SIZE_BYTES: usize = 119; + +#[cfg(any(feature = "std", feature = "alloc"))] +pub type MessageData = lib::std::vec::Vec; +#[cfg(all(not(feature = "std"), not(feature = "alloc")))] +pub type MessageData = lib::std::vec::Vec; + +#[derive(Debug, PartialEq)] +pub struct BinaryAddressedMessage { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub seqno: u8, + pub dest_mmsi: u32, + pub retransmit: bool, + pub dac: u16, + pub fid: u8, + pub data: MessageData, +} + +impl<'a> AisMessageType<'a> for BinaryAddressedMessage { + fn name(&self) -> &'static str { + "Binary Addressed Message" + } + + fn parse(data: &'a [u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} +fn parse_base<'a>(data: &'a [u8]) -> IResult<&'a [u8], BinaryAddressedMessage> { + bits(move |data: (&'a [u8], usize)| -> IResult<_, _> { + let (data, message_type) = take_bits(6u8)(data)?; + let (data, repeat_indicator) = take_bits(2u8)(data)?; + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, seqno) = take_bits(2u8)(data)?; + let (data, dest_mmsi) = take_bits(30u32)(data)?; + let (data, retransmit) = map(take_bits(1u8), u8_to_bool)(data)?; + let (data, _spare) = take_bits::<_, u8, _, _>(1u8)(data)?; + let (data, dac) = take_bits(10u16)(data)?; + let (data, fid) = take_bits(6u8)(data)?; + #[cfg(any(feature = "std", feature = "alloc"))] + let data_owned = data.0.into(); + #[cfg(all(not(feature = "std"), not(feature = "alloc")))] + let data_owned = data.0.try_into().map_err(|_| { + nom::Err::Failure(nom::error::Error::new( + data, + nom::error::ErrorKind::TooLarge, + )) + })?; + Ok(( + (<&[u8]>::default(), 0), + BinaryAddressedMessage { + message_type, + repeat_indicator, + mmsi, + seqno, + dest_mmsi, + retransmit, + dac, + fid, + data: data_owned, + }, + )) + })(data) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unreadable_literal)] + use super::*; + + #[test] + fn test_type6_example_1() { + let bytestream = b"6B?n;be:cbapalgc;i6?Ow4"; + let bitstream = crate::messages::unarmor(bytestream, 2).unwrap(); + let report = BinaryAddressedMessage::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 6); + assert_eq!(report.repeat_indicator, 1); + assert_eq!(report.mmsi, 150834090); + assert_eq!(report.seqno, 3); + assert_eq!(report.dest_mmsi, 313240222); + assert_eq!(report.retransmit, false); + assert_eq!(report.dac, 669); + assert_eq!(report.fid, 11); + + let expected_data: MessageData = [0xeb, 0x2f, 0x11, 0x8f, 0x7f, 0xf1, 0x00] + .into_iter() + .collect(); + assert_eq!(report.data, expected_data); + } + + #[test] + fn test_type6_example_2() { + let bytestream = b"6>jR0600V:C0>da4P106P00"; + let bitstream = crate::messages::unarmor(bytestream, 2).unwrap(); + let report = BinaryAddressedMessage::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 6); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 992509976); + assert_eq!(report.seqno, 0); + assert_eq!(report.dest_mmsi, 2500912); + assert_eq!(report.retransmit, false); + assert_eq!(report.dac, 235); + assert_eq!(report.fid, 10); + + let expected_data: MessageData = [0x44, 0x80, 0x10, 0x06, 0x80, 0x00, 0x00] + .into_iter() + .collect(); + assert_eq!(report.data, expected_data); + } +} diff --git a/src/messages/binary_broadcast_message.rs b/src/messages/binary_broadcast_message.rs index d3c2363..3e8af47 100644 --- a/src/messages/binary_broadcast_message.rs +++ b/src/messages/binary_broadcast_message.rs @@ -70,8 +70,8 @@ impl AssignedMode { } } -fn parse_base<'a> (data: &'a [u8]) -> IResult<&'a [u8], BinaryBroadcastMessage> { - bits (move |data: (&'a [u8], usize)| -> IResult<_, _> { +fn parse_base<'a>(data: &'a [u8]) -> IResult<&'a [u8], BinaryBroadcastMessage> { + bits(move |data: (&'a [u8], usize)| -> IResult<_, _> { let (data, message_type) = take_bits(6u8)(data)?; let (data, repeat_indicator) = take_bits(2u8)(data)?; let (data, mmsi) = take_bits(30u32)(data)?; diff --git a/src/messages/data_link_management_message.rs b/src/messages/data_link_management_message.rs index 1554890..0bad919 100644 --- a/src/messages/data_link_management_message.rs +++ b/src/messages/data_link_management_message.rs @@ -59,7 +59,7 @@ impl<'a> AisMessageType<'a> for DataLinkManagementMessage { } } -fn parse_base<'a> (data: &'a [u8]) -> IResult<&'a [u8], DataLinkManagementMessage> { +fn parse_base<'a>(data: &'a [u8]) -> IResult<&'a [u8], DataLinkManagementMessage> { bits(move |data: (&'a [u8], usize)| -> IResult<_, _> { let (data, message_type) = take_bits(6u8)(data)?; let (data, repeat_indicator) = take_bits(2u8)(data)?; diff --git a/src/messages/long_range_ais_broadcast.rs b/src/messages/long_range_ais_broadcast.rs new file mode 100644 index 0000000..dccafe3 --- /dev/null +++ b/src/messages/long_range_ais_broadcast.rs @@ -0,0 +1,151 @@ +//! Long Range AIS Broadcast message (type 27) +use super::navigation::*; +use super::parsers::*; +use super::position_report::NavigationStatus; +use super::AisMessageType; +use crate::errors::Result; +use nom::bits::{bits, complete::take as take_bits}; +use nom::combinator::map; +use nom::IResult; + +#[derive(Debug, PartialEq)] +pub struct LongRangeAisBroadcastMessage { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub position_accuracy: Accuracy, + pub raim: bool, + pub navigation_status: Option, + pub longitude: Option, + pub latitude: Option, + pub speed_over_ground: Option, + pub course_over_ground: Option, + pub gnss_position_status: bool, +} + +impl<'a> AisMessageType<'a> for LongRangeAisBroadcastMessage { + fn name(&self) -> &'static str { + "Long Range AIS Broadcast message" + } + + fn parse(data: &[u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} + +fn parse_base(data: &[u8]) -> IResult<&[u8], LongRangeAisBroadcastMessage> { + bits(move |data| -> IResult<_, _> { + let (data, message_type) = take_bits(6u8)(data)?; + let (data, repeat_indicator) = take_bits(2u8)(data)?; + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, position_accuracy) = map(take_bits(1u8), Accuracy::parse)(data)?; + let (data, raim) = map(take_bits(1u8), u8_to_bool)(data)?; + let (data, navigation_status) = map(take_bits(4u8), NavigationStatus::parse)(data)?; + + let (data, longitude) = map( + |data| signed_i32(data, 18), + |lon| { + parse_longitude(lon).map(|val| { + if message_type == 27 { + val * 1000.0 + } else { + val + } + }) + }, + )(data)?; + + let (data, latitude) = map( + |data| signed_i32(data, 17), + |lat| { + parse_latitude(lat).map(|val| { + if message_type == 27 { + val * 1000.0 + } else { + val + } + }) + }, + )(data)?; + + let (data, speed_over_ground) = map(take_bits(6u16), parse_speed_over_ground_62)(data)?; + let (data, course_over_ground) = map(take_bits(9u16), parse_cog_511)(data)?; + let (data, gnss_position_status) = map(take_bits(1u8), u8_to_bool)(data)?; + + Ok(( + data, + LongRangeAisBroadcastMessage { + message_type, + repeat_indicator, + mmsi, + position_accuracy, + raim, + navigation_status, + longitude, + latitude, + speed_over_ground, + course_over_ground, + gnss_position_status, + }, + )) + })(data) +} + +/// Parse the speed over ground for Long Range AIS Broadcast Message (type 27) +fn parse_speed_over_ground_62(data: u16) -> Option { + match data { + 63 => None, // Speed not available + _ => Some(data as f32), // Speed in knots (0-62) + } +} + +/// Parse the course over ground for Long Range AIS Broadcast Message (type 27) +fn parse_cog_511(data: u16) -> Option { + match data { + 511 => None, // Course not available + _ => Some(data as f32), // Course in degrees (0-359) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unreadable_literal)] + use crate::test_helpers::f32_equal_naive; + + use super::*; + // use crate::test_helpers::*; + #[test] + fn test_type27_example() { + let bytestream = b"KC5E2b@U19PFdLbMuc5=ROv62<7m"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = LongRangeAisBroadcastMessage::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 27); + assert_eq!(report.repeat_indicator, 1); + assert_eq!(report.mmsi, 206914217); + assert_eq!(report.position_accuracy, Accuracy::Unaugmented); + assert_eq!(report.raim, false); + assert_eq!( + report.navigation_status, + Some(NavigationStatus::NotUnderCommand) + ); + f32_equal_naive(report.longitude.unwrap(), 137.023333); + f32_equal_naive(report.latitude.unwrap(), 4.84); + assert_eq!(report.speed_over_ground, Some(57.0)); + assert_eq!(report.course_over_ground, Some(167.0)); + assert_eq!(report.gnss_position_status, false); + } + + #[test] + fn test_type27_signed_example() { + let bytestream = b"K01;FQh?PbtE3P00"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = LongRangeAisBroadcastMessage::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 27); + assert_eq!(report.mmsi, 1234567); + f32_equal_naive(report.longitude.unwrap(), -13.368334); + f32_equal_naive(report.latitude.unwrap(), -50.121665); + } +} diff --git a/src/messages/mod.rs b/src/messages/mod.rs index 72a20c4..e9c36bc 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -3,26 +3,33 @@ use crate::errors::Result; use crate::lib; use crate::sentence::AisRawData; +pub mod addressed_safety_related; pub mod aid_to_navigation_report; pub mod base_station_report; +pub mod binary_addressed; pub mod binary_broadcast_message; pub mod data_link_management_message; pub mod dgnss_broadcast_binary_message; pub mod extended_class_b_position_report; pub mod interrogation; +pub mod long_range_ais_broadcast; pub mod navigation; #[cfg(all(not(feature = "std"), not(feature = "alloc")))] mod nom_noalloc; mod parsers; pub mod position_report; pub mod radio_status; +pub mod safety_related_acknowledgment; +pub mod safety_related_broadcast; +pub mod standard_aircraft_position_report; pub mod standard_class_b_position_report; pub mod static_and_voyage_related_data; pub mod static_data_report; pub mod types; +pub mod utc_date_inquiry; pub mod utc_date_response; -pub mod standard_aircraft_position_report; pub mod binary_acknowledge; + pub use parsers::message_type; #[cfg(feature = "alloc")] @@ -45,6 +52,12 @@ pub enum AisMessage { UtcDateResponse(utc_date_response::UtcDateResponse), StandardAircraftPositionReport(standard_aircraft_position_report::SARPositionReport), BinaryAcknowledgeMessage(binary_acknowledge::BinaryAcknowledge), + UtcDateInquiry(utc_date_inquiry::UtcDateInquiry), + AddressedSafetyRelatedMessage(addressed_safety_related::AddressedSafetyRelatedMessage), + SafetyRelatedBroadcastMessage(safety_related_broadcast::SafetyRelatedBroadcastMessage), + SafetyRelatedAcknowledgment(safety_related_acknowledgment::SafetyRelatedAcknowledge), + LongRangeAisBroadcastMessage(long_range_ais_broadcast::LongRangeAisBroadcastMessage), + BinaryAddressedMessage(binary_addressed::BinaryAddressedMessage), } /// Trait that describes specific types of AIS messages @@ -74,15 +87,30 @@ pub fn parse(unarmored: &[u8]) -> Result { 7 => Ok(AisMessage::BinaryAcknowledgeMessage( binary_acknowledge::BinaryAcknowledge::parse(unarmored)?, )), + 6 => Ok(AisMessage::BinaryAddressedMessage( + binary_addressed::BinaryAddressedMessage::parse(unarmored)?, + )), 8 => Ok(AisMessage::BinaryBroadcastMessage( binary_broadcast_message::BinaryBroadcastMessage::parse(unarmored)?, )), 9 => Ok(AisMessage::StandardAircraftPositionReport( standard_aircraft_position_report::SARPositionReport::parse(unarmored)?, )), + 10 => Ok(AisMessage::UtcDateInquiry( + utc_date_inquiry::UtcDateInquiry::parse(unarmored)?, + )), 11 => Ok(AisMessage::UtcDateResponse( utc_date_response::UtcDateResponse::parse(unarmored)?, )), + 12 => Ok(AisMessage::AddressedSafetyRelatedMessage( + addressed_safety_related::AddressedSafetyRelatedMessage::parse(unarmored)?, + )), + 13 => Ok(AisMessage::SafetyRelatedAcknowledgment( + safety_related_acknowledgment::SafetyRelatedAcknowledge::parse(unarmored)?, + )), + 14 => Ok(AisMessage::SafetyRelatedBroadcastMessage( + safety_related_broadcast::SafetyRelatedBroadcastMessage::parse(unarmored)?, + )), 15 => Ok(AisMessage::Interrogation( interrogation::Interrogation::parse(unarmored)?, )), @@ -104,6 +132,9 @@ pub fn parse(unarmored: &[u8]) -> Result { 24 => Ok(AisMessage::StaticDataReport( static_data_report::StaticDataReport::parse(unarmored)?, )), + 27 => Ok(AisMessage::LongRangeAisBroadcastMessage( + long_range_ais_broadcast::LongRangeAisBroadcastMessage::parse(unarmored)?, + )), #[cfg(any(feature = "std", feature = "alloc"))] _ => Err(format!("Unimplemented type: {}", result).into()), #[cfg(all(not(feature = "std"), not(feature = "alloc")))] diff --git a/src/messages/position_report.rs b/src/messages/position_report.rs index 00f3c1a..e6d4f80 100644 --- a/src/messages/position_report.rs +++ b/src/messages/position_report.rs @@ -100,7 +100,7 @@ pub enum NavigationStatus { } impl NavigationStatus { - fn parse(data: u8) -> Option { + pub fn parse(data: u8) -> Option { match data { 0 => Some(Self::UnderWayUsingEngine), 1 => Some(Self::AtAnchor), diff --git a/src/messages/safety_related_acknowledgment.rs b/src/messages/safety_related_acknowledgment.rs new file mode 100644 index 0000000..6602008 --- /dev/null +++ b/src/messages/safety_related_acknowledgment.rs @@ -0,0 +1,93 @@ +//! Safety-Related Acknowledge (type 13) +#[cfg(all(not(feature = "std"), not(feature = "alloc")))] +use super::nom_noalloc::many_m_n; +use super::AisMessageType; +use crate::errors::Result; +use crate::lib; +use nom::bits::{bits, complete::take as take_bits}; +#[cfg(any(feature = "std", feature = "alloc"))] +use nom::multi::many_m_n; +use nom::IResult; + +#[derive(Debug, PartialEq, Eq)] +pub struct Acknowledgement { + pub mmsi: u32, + pub seq_num: u8, +} + +impl Acknowledgement { + pub fn parse(data: (&[u8], usize)) -> IResult<(&[u8], usize), Self> { + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, seq_num) = take_bits(2u8)(data)?; + Ok((data, Self { mmsi, seq_num })) + } +} + +#[cfg(any(feature = "std", feature = "alloc"))] +pub type AcknowledgementList = lib::std::vec::Vec; +#[cfg(all(not(feature = "std"), not(feature = "alloc")))] +pub type AcknowledgementList = lib::std::vec::Vec; + +#[derive(Debug, PartialEq, Eq)] +pub struct SafetyRelatedAcknowledge { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub acks: AcknowledgementList, +} + +impl<'a> AisMessageType<'a> for SafetyRelatedAcknowledge { + fn name(&self) -> &'static str { + "Safety-Related Acknowledge" + } + + fn parse(data: &'a [u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} + +fn parse_base<'a>(data: &'a [u8]) -> IResult<&'a [u8], SafetyRelatedAcknowledge> { + bits(move |data: (&'a [u8], usize)| -> IResult<_, _> { + let (data, message_type) = take_bits(6u8)(data)?; + let (data, repeat_indicator) = take_bits(2u8)(data)?; + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, _spare) = take_bits::<_, u8, _, _>(2u8)(data)?; + + #[cfg(any(feature = "std", feature = "alloc"))] + let (data, acks) = many_m_n(1, 4, Acknowledgement::parse)(data)?; + #[cfg(all(not(feature = "std"), not(feature = "alloc")))] + let (data, acks) = many_m_n::<_, _, _, _, 4>(1, Acknowledgement::parse)(data)?; + + Ok(( + data, + SafetyRelatedAcknowledge { + message_type, + repeat_indicator, + mmsi, + acks, + }, + )) + })(data) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unreadable_literal)] + use super::*; + + #[test] + fn test_type13_example() { + let bytestream = b"=39UOj0jFs9R"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = SafetyRelatedAcknowledge::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 13); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 211378120); + assert_eq!(report.acks.len(), 1); + let ack = &report.acks[0]; + assert_eq!(ack.mmsi, 211217560); + assert_eq!(ack.seq_num, 2); + } +} diff --git a/src/messages/safety_related_broadcast.rs b/src/messages/safety_related_broadcast.rs new file mode 100644 index 0000000..979b8ea --- /dev/null +++ b/src/messages/safety_related_broadcast.rs @@ -0,0 +1,73 @@ +//! Safety-Related Broadcast Message (type 14) + +use super::parsers::*; +use super::AisMessageType; +use crate::errors::Result; +use nom::bits::{bits, complete::take as take_bits}; +use nom::IResult; + +#[derive(Debug, PartialEq)] +pub struct SafetyRelatedBroadcastMessage { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub text: AsciiString, +} + +impl<'a> AisMessageType<'a> for SafetyRelatedBroadcastMessage { + fn name(&self) -> &'static str { + "Safety-Related Broadcast Message" + } + + fn parse(data: &'a [u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} + +fn parse_base(data: &[u8]) -> IResult<&[u8], SafetyRelatedBroadcastMessage> { + bits(move |data| -> IResult<_, _> { + let (data, message_type) = take_bits(6u8)(data)?; + let (data, repeat_indicator) = take_bits(2u8)(data)?; + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, _spare) = take_bits::<_, u8, _, _>(2u8)(data)?; + + // Ensure there are enough bits remaining for text + let remaining_bits = remaining_bits(data); + if remaining_bits < 6 { + return Err(nom::Err::Error(nom::error::Error::new( + data, + nom::error::ErrorKind::Eof, + ))); + } + + let (data, text) = parse_6bit_ascii(data, remaining_bits)?; + + Ok(( + data, + SafetyRelatedBroadcastMessage { + message_type, + repeat_indicator, + mmsi, + text, + }, + )) + })(data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_type14_example() { + let bytestream = b">5?Per18=HB1U:1@E=B0m AisMessageType<'a> for SARPositionReport { fn name(&self) -> &'static str { "Standard SAR Aircraft Position Report" @@ -42,7 +40,6 @@ impl<'a> AisMessageType<'a> for SARPositionReport { } } - fn parse_base(data: &[u8]) -> IResult<&[u8], SARPositionReport> { bits(move |data| -> IResult<_, _> { let (data, message_type) = take_bits(6u8)(data)?; @@ -96,7 +93,7 @@ fn parse_altitude(data: u16) -> Option { /// Parse the speed over ground for SAR Position Report (type 9) fn parse_speed_over_ground_sar(data: u16) -> Option { match data { - 1023 => None, // Speed not available + 1023 => None, // Speed not available 1022 => Some(1022.0), // 1022 knots or higher _ => Some(data as f32), } @@ -137,7 +134,7 @@ mod tests { } } else { panic!("Expected SOTDMA message"); - } + } assert!(!report.raim); } } diff --git a/src/messages/utc_date_inquiry.rs b/src/messages/utc_date_inquiry.rs new file mode 100644 index 0000000..c3c155a --- /dev/null +++ b/src/messages/utc_date_inquiry.rs @@ -0,0 +1,74 @@ +//! UTC/Date Inquiry (type 10) +use super::AisMessageType; +use crate::errors::Result; +use nom::bits::{bits, complete::take as take_bits}; +use nom::IResult; + +#[derive(Debug, PartialEq, Eq)] +pub struct UtcDateInquiry { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub dest_mmsi: u32, +} + +impl<'a> AisMessageType<'a> for UtcDateInquiry { + fn name(&self) -> &'static str { + "UTC/Date Inquiry" + } + + fn parse(data: &'a [u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} + +fn parse_base(data: &[u8]) -> IResult<&[u8], UtcDateInquiry> { + bits(move |data| -> IResult<_, _> { + let (data, message_type) = take_bits(6u8)(data)?; + let (data, repeat_indicator) = take_bits(2u8)(data)?; + let (data, mmsi) = take_bits(30u32)(data)?; + let (data, _spare1) = take_bits::<_, u8, _, _>(2u8)(data)?; + let (data, dest_mmsi) = take_bits(30u32)(data)?; + let (data, _spare2) = take_bits::<_, u8, _, _>(2u8)(data)?; + + Ok(( + data, + UtcDateInquiry { + message_type, + repeat_indicator, + mmsi, + dest_mmsi, + }, + )) + })(data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_type10_example_a() { + let bytestream = b":5MlU41GMK6@"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = UtcDateInquiry::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 10); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 366814480); + assert_eq!(report.dest_mmsi, 366832740); + } + + #[test] + fn test_type10_example_b() { + let bytestream = b":6TMCD1GOS60"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = UtcDateInquiry::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 10); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 440882000); + assert_eq!(report.dest_mmsi, 366972000); + } +}