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#[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#[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#[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
83pub trait AisEncodable {
85 fn to_sentences(&self, options: AisTransmitOptions) -> Result<Vec<String>, EncodeError>;
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum PositionTimestamp {
91 Exact(u8),
93 NotAvailable,
95 ManualInput,
97 DeadReckoning,
99 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}