Skip to main content

nmea_kit/ais/
transmit.rs

1use crate::ais::armor::encode_armor;
2use crate::{EncodeError, encode_frame};
3
4pub mod class_a;
5pub mod class_b;
6pub mod safety;
7pub mod stations;
8
9pub use class_a::*;
10pub use class_b::*;
11pub use safety::*;
12pub use stations::*;
13
14const MAX_FRAGMENT_PAYLOAD_CHARS: usize = 60;
15const MAX_FRAGMENTS: usize = 5;
16const MAX_PAYLOAD_BITS: usize = 1_152;
17
18/// AIS VDL channel for an emitted AIVDM/AIVDO sentence.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AisChannel {
21    A,
22    B,
23}
24
25impl AisChannel {
26    fn as_str(self) -> &'static str {
27        match self {
28            Self::A => "A",
29            Self::B => "B",
30        }
31    }
32}
33
34/// AIS NMEA envelope type.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum AisSentenceKind {
37    Vdm,
38    Vdo,
39}
40
41impl AisSentenceKind {
42    fn as_str(self) -> &'static str {
43        match self {
44            Self::Vdm => "VDM",
45            Self::Vdo => "VDO",
46        }
47    }
48}
49
50/// NMEA framing options for a stateless AIS transmission.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct AisTransmitOptions {
53    pub sentence_kind: AisSentenceKind,
54    pub channel: AisChannel,
55    pub sequence_id: Option<u8>,
56}
57
58impl AisTransmitOptions {
59    pub const fn vdm(channel: AisChannel) -> Self {
60        Self {
61            sentence_kind: AisSentenceKind::Vdm,
62            channel,
63            sequence_id: None,
64        }
65    }
66
67    pub const fn vdo(channel: AisChannel) -> Self {
68        Self {
69            sentence_kind: AisSentenceKind::Vdo,
70            channel,
71            sequence_id: None,
72        }
73    }
74
75    pub const fn with_sequence_id(self, sequence_id: u8) -> Self {
76        Self {
77            sequence_id: Some(sequence_id),
78            ..self
79        }
80    }
81}
82
83/// Encode an AIS model into one or more complete AIVDM/AIVDO sentences.
84pub trait AisEncodable {
85    fn to_sentences(&self, options: AisTransmitOptions) -> Result<Vec<String>, EncodeError>;
86}
87
88/// Timestamp status carried by AIS position reports.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum PositionTimestamp {
91    /// UTC second at which the report was generated (0-59).
92    Exact(u8),
93    /// UTC time is not available.
94    NotAvailable,
95    /// Position was entered manually.
96    ManualInput,
97    /// Position comes from estimated or dead-reckoning navigation.
98    DeadReckoning,
99    /// The electronic position-fixing system is inoperative.
100    Inoperative,
101}
102
103fn encode_payload(bits: &[u8], options: AisTransmitOptions) -> Result<Vec<String>, EncodeError> {
104    if bits.is_empty() {
105        return Err(EncodeError::InvalidAisField("payload"));
106    }
107    if bits.len() > MAX_PAYLOAD_BITS {
108        return Err(EncodeError::TooManyAisFragments);
109    }
110    if matches!(options.sequence_id, Some(id) if id > 9) {
111        return Err(EncodeError::InvalidAisField("sequence_id"));
112    }
113
114    let (payload, fill_bits) = encode_armor(bits);
115    let fragment_count = payload.len().div_ceil(MAX_FRAGMENT_PAYLOAD_CHARS);
116    if fragment_count > MAX_FRAGMENTS {
117        return Err(EncodeError::TooManyAisFragments);
118    }
119    let sequence_id = if fragment_count > 1 {
120        options
121            .sequence_id
122            .ok_or(EncodeError::MissingAisSequenceId)?
123    } else {
124        0
125    };
126
127    let total = fragment_count.to_string();
128    let sequence = sequence_id.to_string();
129    let mut sentences = Vec::with_capacity(fragment_count);
130
131    for (index, payload_fragment) in payload
132        .as_bytes()
133        .chunks(MAX_FRAGMENT_PAYLOAD_CHARS)
134        .enumerate()
135    {
136        let fragment = core::str::from_utf8(payload_fragment)
137            .map_err(|_| EncodeError::InvalidAisField("payload"))?;
138        let number = (index + 1).to_string();
139        let fill = if index + 1 == fragment_count {
140            fill_bits.to_string()
141        } else {
142            String::from("0")
143        };
144        let sequence_field = if fragment_count > 1 {
145            sequence.as_str()
146        } else {
147            ""
148        };
149        let fields = [
150            total.as_str(),
151            number.as_str(),
152            sequence_field,
153            options.channel.as_str(),
154            fragment,
155            fill.as_str(),
156        ];
157        sentences.push(encode_frame(
158            '!',
159            "AI",
160            options.sentence_kind.as_str(),
161            &fields,
162        )?);
163    }
164
165    Ok(sentences)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::{EncodeError, parse_frame};
172
173    #[test]
174    fn payload_of_61_characters_is_fragmented_at_60() {
175        let bits = vec![0; 61 * 6];
176        let options = AisTransmitOptions::vdm(AisChannel::A).with_sequence_id(7);
177        let lines = encode_payload(&bits, options).expect("encode AIS payload");
178
179        assert_eq!(lines.len(), 2);
180        assert!(lines[0].starts_with("!AIVDM,2,1,7,A,"));
181        assert!(lines[1].starts_with("!AIVDM,2,2,7,A,"));
182        assert!(lines.iter().all(|line| line.len() <= 82));
183        assert!(lines.iter().all(|line| parse_frame(line).is_ok()));
184    }
185
186    #[test]
187    fn payload_requiring_six_fragments_is_rejected() {
188        let bits = vec![0; 301 * 6];
189        let options = AisTransmitOptions::vdm(AisChannel::A).with_sequence_id(7);
190
191        assert_eq!(
192            encode_payload(&bits, options),
193            Err(EncodeError::TooManyAisFragments)
194        );
195    }
196}