diff --git a/src/messages/mod.rs b/src/messages/mod.rs index d6b9046..811df4f 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -21,6 +21,7 @@ pub mod static_and_voyage_related_data; pub mod static_data_report; pub mod types; pub mod utc_date_response; +pub mod standard_aircraft_position_report; pub use parsers::message_type; @@ -42,6 +43,7 @@ pub enum AisMessage { AidToNavigationReport(aid_to_navigation_report::AidToNavigationReport), StaticDataReport(static_data_report::StaticDataReport), UtcDateResponse(utc_date_response::UtcDateResponse), + StandardAircraftPositionReport(standard_aircraft_position_report::SARPositionReport), } /// Trait that describes specific types of AIS messages @@ -71,6 +73,9 @@ pub fn parse(unarmored: &[u8]) -> Result { 8 => Ok(AisMessage::BinaryBroadcastMessage( binary_broadcast_message::BinaryBroadcastMessage::parse(unarmored)?, )), + 9 => Ok(AisMessage::StandardAircraftPositionReport( + standard_aircraft_position_report::SARPositionReport::parse(unarmored)?, + )), 11 => Ok(AisMessage::UtcDateResponse( utc_date_response::UtcDateResponse::parse(unarmored)?, )), diff --git a/src/messages/radio_status.rs b/src/messages/radio_status.rs index 1f6d32d..129897a 100644 --- a/src/messages/radio_status.rs +++ b/src/messages/radio_status.rs @@ -126,7 +126,7 @@ impl ItdmaMessage { pub fn parse_radio(input: (&[u8], usize), msg_type: u8) -> IResult<(&[u8], usize), RadioStatus> { match msg_type { - 1 | 2 | 4 | 11 => SotdmaMessage::parse(input), + 1 | 2 | 4 | 11 | 9 => SotdmaMessage::parse(input), 3 => ItdmaMessage::parse(input), _ => Err(nom::Err::Failure(nom::error::Error::new( input, diff --git a/src/messages/standard_aircraft_position_report.rs b/src/messages/standard_aircraft_position_report.rs new file mode 100644 index 0000000..e111874 --- /dev/null +++ b/src/messages/standard_aircraft_position_report.rs @@ -0,0 +1,143 @@ +//! Standard SAR Aircraft Position Report (type 9) +use super::navigation::*; +use super::parsers::*; +use super::radio_status::*; + +use super::AisMessageType; +use crate::errors::Result; +use nom::bits::{bits, complete::take as take_bits}; +use nom::combinator::map; +use nom::IResult; +use super::types::Dte; +use super::types::AssignedMode; + + +#[derive(Debug, PartialEq)] +pub struct SARPositionReport { + pub message_type: u8, + pub repeat_indicator: u8, + pub mmsi: u32, + pub altitude: Option, + pub speed_over_ground: Option, + pub position_accuracy: Accuracy, + pub longitude: Option, + pub latitude: Option, + pub course_over_ground: Option, + pub timestamp: u8, + pub dte: Dte, + pub assigned_mode: AssignedMode, + pub raim: bool, + pub radio_status: RadioStatus, +} + + +impl<'a> AisMessageType<'a> for SARPositionReport { + fn name(&self) -> &'static str { + "Standard SAR Aircraft Position Report" + } + + fn parse(data: &'a [u8]) -> Result { + let (_, report) = parse_base(data)?; + Ok(report) + } +} + + +fn parse_base(data: &[u8]) -> IResult<&[u8], SARPositionReport> { + 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, altitude) = map(take_bits(12u16), parse_altitude)(data)?; + let (data, speed_over_ground) = map(take_bits(10u16), parse_speed_over_ground_sar)(data)?; + let (data, position_accuracy) = map(take_bits(1u8), Accuracy::parse)(data)?; + let (data, longitude) = map(|data| signed_i32(data, 28), parse_longitude)(data)?; + let (data, latitude) = map(|data| signed_i32(data, 27), parse_latitude)(data)?; + let (data, course_over_ground) = map(take_bits(12u16), parse_cog)(data)?; + let (data, timestamp) = take_bits(6u8)(data)?; + let (data, _regional_reserved) = take_bits::<_, u8, _, _>(8u8)(data)?; + let (data, dte) = map(take_bits::<_, u8, _, _>(1u8), Into::into)(data)?; + let (data, _spare) = take_bits::<_, u8, _, _>(3u8)(data)?; + let (data, assigned_mode) = map(take_bits(1u8), AssignedMode::parse)(data)?; + let (data, raim) = map(take_bits(1u8), u8_to_bool)(data)?; + let (data, radio_status) = parse_radio(data, message_type)?; + + Ok(( + data, + SARPositionReport { + message_type, + repeat_indicator, + mmsi, + altitude, + speed_over_ground, + position_accuracy, + longitude, + latitude, + course_over_ground, + timestamp, + dte, + assigned_mode, + raim, + radio_status, + }, + )) + })(data) +} + +/// Parse the altitude field +fn parse_altitude(data: u16) -> Option { + match data { + 4095 => None, // Altitude not available + 4094 => Some(4094), // 4094 meters or higher + _ => Some(data), + } +} + +/// 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 + 1022 => Some(1022.0), // 1022 knots or higher + _ => Some(data as f32), + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unreadable_literal)] + use super::*; + use crate::messages::radio_status::{SubMessage, SyncState}; + use crate::test_helpers::*; + + #[test] + fn test_type9_example() { + let bytestream = b"91b55wi;hbOS@OdQAC062Ch2089h"; + let bitstream = crate::messages::unarmor(bytestream, 0).unwrap(); + let report = SARPositionReport::parse(bitstream.as_ref()).unwrap(); + + assert_eq!(report.message_type, 9); + assert_eq!(report.repeat_indicator, 0); + assert_eq!(report.mmsi, 111232511); + assert_eq!(report.altitude, Some(303)); + assert_eq!(report.speed_over_ground, Some(42.0)); + assert_eq!(report.position_accuracy, Accuracy::Unaugmented); + f32_equal_naive(report.longitude.unwrap(), -6.2788434); + f32_equal_naive(report.latitude.unwrap(), 58.144); + assert_eq!(report.course_over_ground, Some(154.5)); + assert_eq!(report.timestamp, 15); + assert_eq!(report.dte, Dte::NotReady); + if let RadioStatus::Sotdma(radio_status) = report.radio_status { + assert_eq!(radio_status.sync_state, SyncState::UtcDirect); + assert_eq!(radio_status.slot_timeout, 1); + if let SubMessage::UtcHourAndMinute(hour, minute) = radio_status.sub_message { + assert_eq!(hour, 0); + assert_eq!(minute, 14); + } else { + panic!("Expected UTC Hour and Minute submessage"); + } + } else { + panic!("Expected SOTDMA message"); + } + assert!(!report.raim); + } +}