#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod bits;
pub mod error;
pub mod message;
pub mod nmea;
pub mod string;
pub use error::AisError;
pub use message::AisMessage;
pub use message::common::*;
pub use nmea::{Channel, CompleteMessage, FragmentAssembler, Sentence};
pub fn decode_line(line: &str) -> Result<AisMessage, AisError> {
let sentence = Sentence::parse(line)?;
if sentence.fragment_count != 1 {
return Err(AisError::IncompleteFragment);
}
decode_payload(sentence.payload, sentence.fill_bits)
}
pub fn decode_payload(armored: &[u8], fill_bits: u8) -> Result<AisMessage, AisError> {
let mut reader = bits::BitReader::new(armored, fill_bits);
AisMessage::decode(&mut reader).map_err(AisError::from)
}
pub fn encode_payload<'a>(
message: &AisMessage,
buf: &'a mut [u8],
) -> Result<(&'a [u8], u8), AisError> {
let mut writer = bits::BitWriter::new(buf);
message.encode(&mut writer)?;
Ok(writer.finish()?)
}
const MAX_SINGLE_SENTENCE_PAYLOAD_BYTES: usize = 128;
pub fn encode_line<'a>(
message: &AisMessage,
channel: Channel,
is_own_ship: bool,
buf: &'a mut [u8],
) -> Result<&'a [u8], AisError> {
let mut payload_buf = [0u8; MAX_SINGLE_SENTENCE_PAYLOAD_BYTES];
let (payload, fill_bits) = encode_payload(message, &mut payload_buf)?;
let sentence = Sentence {
is_own_ship,
fragment_count: 1,
fragment_number: 1,
seq_id: None,
channel,
payload,
fill_bits,
};
Ok(sentence.format(buf)?)
}
#[derive(Debug)]
pub struct LineDecoder<const N: usize> {
assembler: FragmentAssembler<N>,
}
impl<const N: usize> Default for LineDecoder<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> LineDecoder<N> {
#[must_use]
pub const fn new() -> Self {
Self {
assembler: FragmentAssembler::new(),
}
}
pub fn feed(&mut self, line: &str) -> Result<Option<AisMessage>, AisError> {
let sentence = Sentence::parse(line)?;
if sentence.fragment_count == 1 {
return decode_payload(sentence.payload, sentence.fill_bits).map(Some);
}
match self.assembler.push(&sentence) {
Ok(Some(complete)) => decode_payload(complete.armored, complete.fill_bits).map(Some),
Ok(None) => Ok(None),
Err(e) => {
self.assembler.reset();
Err(AisError::from(e))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_a_real_position_report() {
let msg = decode_line("!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C").unwrap();
assert_eq!(msg.message_type(), 1);
let AisMessage::PositionReportClassA(p) = msg else {
panic!("expected PositionReportClassA");
};
assert_eq!(p.mmsi.raw(), 366_053_209);
assert_eq!(
p.navigation_status,
NavigationStatus::RestrictedManoeuvrability
);
assert!((p.longitude.as_degrees().unwrap() - (-122.341_618_333)).abs() < 1e-6);
assert!((p.latitude.as_degrees().unwrap() - 37.802_118_333).abs() < 1e-6);
}
#[test]
fn common_accessors_work_directly_on_ais_message() {
let msg = decode_line("!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C").unwrap();
assert_eq!(msg.mmsi().raw(), 366_053_209);
assert_eq!(
msg.navigation_status(),
Some(NavigationStatus::RestrictedManoeuvrability)
);
let position = msg.position().unwrap();
assert!((position.latitude - 37.802_118_333).abs() < 1e-6);
assert!((position.longitude - (-122.341_618_333)).abs() < 1e-6);
}
#[test]
fn multi_fragment_sentence_reports_incomplete() {
let err = decode_line("!AIVDM,2,1,7,B,15M67FC000,0*6C").unwrap_err();
assert_eq!(err, AisError::IncompleteFragment);
}
#[test]
fn encode_round_trips_a_real_position_report() {
let original_payload = b"15M67FC000G?ufbE`FepT@3n00Sa";
let msg = decode_payload(original_payload, 0).unwrap();
let mut buf = [0u8; 32];
let (armored, fill_bits) = encode_payload(&msg, &mut buf).unwrap();
assert_eq!(fill_bits, 0);
assert_eq!(armored, original_payload);
let re_decoded = decode_payload(armored, fill_bits).unwrap();
assert_eq!(re_decoded, msg);
}
#[test]
fn encode_line_round_trips_a_real_position_report() {
let line = "!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C";
let msg = decode_line(line).unwrap();
let mut buf = [0u8; 64];
let encoded = encode_line(&msg, Channel::B, false, &mut buf).unwrap();
assert_eq!(encoded, line.as_bytes());
let re_decoded = decode_line(core::str::from_utf8(encoded).unwrap()).unwrap();
assert_eq!(re_decoded, msg);
}
#[test]
fn encode_line_emits_aivdo_for_own_ship() {
let line = "!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C";
let msg = decode_line(line).unwrap();
let mut buf = [0u8; 64];
let encoded = encode_line(&msg, Channel::A, true, &mut buf).unwrap();
assert!(encoded.starts_with(b"!AIVDO,"));
let reparsed = Sentence::parse(core::str::from_utf8(encoded).unwrap()).unwrap();
assert!(reparsed.is_own_ship);
assert_eq!(reparsed.channel, Channel::A);
}
#[test]
fn line_decoder_decodes_single_fragment_immediately() {
let mut decoder = LineDecoder::<64>::new();
let msg = decoder
.feed("!AIVDM,1,1,,B,15M67FC000G?ufbE`FepT@3n00Sa,0*5C")
.unwrap()
.unwrap();
assert_eq!(msg.mmsi().raw(), 366_053_209);
}
#[test]
fn line_decoder_reassembles_multi_fragment_sentences() {
let mut decoder = LineDecoder::<64>::new();
assert_eq!(
decoder.feed("!AIVDM,2,1,7,B,15M67FC000,0*6C").unwrap(),
None
);
let msg = decoder
.feed("!AIVDM,2,2,7,B,0G?ufbE`FepT@3n00Sa,0*26")
.unwrap()
.unwrap();
assert_eq!(msg.mmsi().raw(), 366_053_209);
}
#[test]
fn line_decoder_resyncs_after_an_invalid_fragment() {
let mut decoder = LineDecoder::<64>::new();
let err = decoder
.feed("!AIVDM,2,2,7,B,0G?ufbE`FepT@3n00Sa,0*26")
.unwrap_err();
assert_eq!(
err,
AisError::Fragment(crate::error::FragmentError::OutOfOrder)
);
assert_eq!(
decoder.feed("!AIVDM,2,1,7,B,15M67FC000,0*6C").unwrap(),
None
);
let msg = decoder
.feed("!AIVDM,2,2,7,B,0G?ufbE`FepT@3n00Sa,0*26")
.unwrap()
.unwrap();
assert_eq!(msg.mmsi().raw(), 366_053_209);
}
}