1use core::fmt;
5
6use kinavis_kernel::event::TargetId;
7use kinavis_kernel::units::Distance;
8use kinavis_kernel::InlineStr;
9
10use crate::bits::Bits;
11use crate::error::AisError;
12use crate::fields::{byte, counts_per, value_of, Fields};
13use crate::vessel::{Dimensions, PositionFixingDevice, ShipType};
14
15const NAME_CHARS: usize = 20;
17const CALLSIGN_CHARS: usize = 7;
18const VENDOR_CHARS: usize = 3;
19
20const NO_IMO: u32 = 0;
22const NO_DRAUGHT: u32 = 0;
23
24#[derive(Debug, Clone, Copy, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct StaticAndVoyageData {
31 pub mmsi: TargetId,
33 pub ais_version: u8,
35 pub imo: Option<u32>,
37 pub callsign: Option<InlineStr<CALLSIGN_CHARS>>,
39 pub name: Option<InlineStr<NAME_CHARS>>,
41 pub ship_type: Option<ShipType>,
43 pub dimensions: Option<Dimensions>,
45 pub fixing_device: Option<PositionFixingDevice>,
47 pub eta: Eta,
49 pub draught: Option<Distance>,
52 pub destination: Option<InlineStr<NAME_CHARS>>,
54 pub data_terminal_ready: bool,
56}
57
58impl StaticAndVoyageData {
59 const NEEDED: usize = 424;
61
62 pub(crate) fn decode(bits: &Bits) -> Result<Self, AisError> {
63 let field = Fields::of(bits, Self::NEEDED)?;
64 Ok(Self {
65 mmsi: TargetId::new(field.unsigned(8, 30)),
66 ais_version: byte(field.unsigned(38, 2)),
67 imo: imo(field.unsigned(40, 30)),
68 callsign: field.text(70, CALLSIGN_CHARS),
69 name: field.text(112, NAME_CHARS),
70 ship_type: ShipType::from_code(byte(field.unsigned(232, 8))),
71 dimensions: Dimensions::from_fields(
72 field.unsigned(240, 9),
73 field.unsigned(249, 9),
74 field.unsigned(258, 6),
75 field.unsigned(264, 6),
76 )?,
77 fixing_device: PositionFixingDevice::from_code(byte(field.unsigned(270, 4))),
78 eta: Eta::from_fields(
79 field.unsigned(274, 4),
80 field.unsigned(278, 5),
81 field.unsigned(283, 5),
82 field.unsigned(288, 6),
83 ),
84 draught: draught(field.unsigned(294, 8))?,
85 destination: field.text(302, NAME_CHARS),
86 data_terminal_ready: !field.bit(422),
87 })
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct Eta {
99 pub month: Option<u8>,
101 pub day: Option<u8>,
103 pub hour: Option<u8>,
105 pub minute: Option<u8>,
107}
108
109impl Eta {
110 pub const NONE: Self = Self {
112 month: None,
113 day: None,
114 hour: None,
115 minute: None,
116 };
117
118 fn from_fields(month: u32, day: u32, hour: u32, minute: u32) -> Self {
119 Self {
120 month: ((1..=12).contains(&month)).then_some(byte(month)),
121 day: ((1..=31).contains(&day)).then_some(byte(day)),
122 hour: (hour <= 23).then_some(byte(hour)),
123 minute: (minute <= 59).then_some(byte(minute)),
124 }
125 }
126
127 #[must_use]
129 pub const fn is_given(&self) -> bool {
130 self.month.is_some() || self.day.is_some() || self.hour.is_some() || self.minute.is_some()
131 }
132}
133
134impl fmt::Display for Eta {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 let part = |f: &mut fmt::Formatter<'_>, value: Option<u8>| match value {
138 Some(value) => write!(f, "{value:02}"),
139 None => f.write_str("??"),
140 };
141 part(f, self.month)?;
142 f.write_str("-")?;
143 part(f, self.day)?;
144 f.write_str(" ")?;
145 part(f, self.hour)?;
146 f.write_str(":")?;
147 part(f, self.minute)
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct StaticDataReport {
155 pub mmsi: TargetId,
157 pub part: StaticDataPart,
159}
160
161impl StaticDataReport {
162 const PART_A_NEEDED: usize = 160;
164 const PART_B_NEEDED: usize = 168;
165
166 pub(crate) fn decode(bits: &Bits) -> Result<Option<Self>, AisError> {
168 let field = Fields::of(bits, 40)?;
169 let mmsi = TargetId::new(field.unsigned(8, 30));
170 let part = match field.unsigned(38, 2) {
171 0 => {
172 let field = Fields::of(bits, Self::PART_A_NEEDED)?;
173 StaticDataPart::A {
174 name: field.text(40, NAME_CHARS),
175 }
176 }
177 1 => {
178 let field = Fields::of(bits, Self::PART_B_NEEDED)?;
179 StaticDataPart::B(ClassBStaticData::decode(&field, mmsi)?)
180 }
181 _ => return Ok(None),
182 };
183 Ok(Some(Self { mmsi, part }))
184 }
185}
186
187#[non_exhaustive]
191#[derive(Debug, Clone, Copy, PartialEq)]
192#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
193pub enum StaticDataPart {
194 A {
196 name: Option<InlineStr<NAME_CHARS>>,
198 },
199 B(ClassBStaticData),
201}
202
203#[derive(Debug, Clone, Copy, PartialEq)]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub struct ClassBStaticData {
207 pub ship_type: Option<ShipType>,
209 pub vendor_id: Option<InlineStr<VENDOR_CHARS>>,
211 pub unit_model: u8,
213 pub serial_number: u32,
215 pub callsign: Option<InlineStr<CALLSIGN_CHARS>>,
217 pub dimensions: Option<Dimensions>,
220 pub mother_ship: Option<TargetId>,
222 pub fixing_device: Option<PositionFixingDevice>,
224}
225
226impl ClassBStaticData {
227 fn decode(field: &Fields<'_>, mmsi: TargetId) -> Result<Self, AisError> {
228 let carried = is_carried_craft(mmsi);
229 Ok(Self {
230 ship_type: ShipType::from_code(byte(field.unsigned(40, 8))),
231 vendor_id: field.text(48, VENDOR_CHARS),
232 unit_model: byte(field.unsigned(66, 4)),
233 serial_number: field.unsigned(70, 20),
234 callsign: field.text(90, CALLSIGN_CHARS),
235 dimensions: if carried {
236 None
237 } else {
238 Dimensions::from_fields(
239 field.unsigned(132, 9),
240 field.unsigned(141, 9),
241 field.unsigned(150, 6),
242 field.unsigned(156, 6),
243 )?
244 },
245 mother_ship: carried.then(|| TargetId::new(field.unsigned(132, 30))),
246 fixing_device: PositionFixingDevice::from_code(byte(field.unsigned(162, 4))),
247 })
248 }
249}
250
251const fn is_carried_craft(mmsi: TargetId) -> bool {
254 mmsi.number() / 10_000_000 == 98
255}
256
257fn imo(field: u32) -> Option<u32> {
258 (field != NO_IMO).then_some(field)
259}
260
261fn draught(field: u32) -> Result<Option<Distance>, AisError> {
262 if field == NO_DRAUGHT {
263 return Ok(None);
264 }
265 Distance::from_metres(f64::from(field) / counts_per::TENTH)
267 .map(Some)
268 .map_err(value_of("draught"))
269}