Skip to main content

kinavis_ais/
static_data.rs

1//! Class A static and voyage data (message 5) and class B static data report
2//! (message 24).
3
4use 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
15/// Character counts of the text fields.
16const NAME_CHARS: usize = 20;
17const CALLSIGN_CHARS: usize = 7;
18const VENDOR_CHARS: usize = 3;
19
20/// Field values meaning "not available".
21const NO_IMO: u32 = 0;
22const NO_DRAUGHT: u32 = 0;
23
24/// Class A static and voyage data: message 5.
25///
26/// Every field that can be "not available" is an `Option`; text fields are
27/// `None`, not empty, when entirely padding.
28#[derive(Debug, Clone, Copy, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct StaticAndVoyageData {
31    /// MMSI.
32    pub mmsi: TargetId,
33    /// AIS version: 0 = M.1371-1, 1 = M.1371-3, 2 = M.1371-5, 3 = later.
34    pub ais_version: u8,
35    /// IMO number.
36    pub imo: Option<u32>,
37    /// Call sign, up to 7 characters.
38    pub callsign: Option<InlineStr<CALLSIGN_CHARS>>,
39    /// Name, up to 20 characters.
40    pub name: Option<InlineStr<NAME_CHARS>>,
41    /// Ship and cargo type.
42    pub ship_type: Option<ShipType>,
43    /// Dimensions around the reported position.
44    pub dimensions: Option<Dimensions>,
45    /// Position fixing device.
46    pub fixing_device: Option<PositionFixingDevice>,
47    /// ETA, UTC.
48    pub eta: Eta,
49    /// Maximum present static draught, 0.1 m resolution. 25.5 m means 25.5 m or
50    /// more.
51    pub draught: Option<Distance>,
52    /// Destination, up to 20 characters.
53    pub destination: Option<InlineStr<NAME_CHARS>>,
54    /// DTE ready; stations without a DTE report not ready.
55    pub data_terminal_ready: bool,
56}
57
58impl StaticAndVoyageData {
59    /// Message length: 424 bits.
60    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/// ETA, UTC, as far as given.
92///
93/// No year; each part may be absent independently (day known, hour not). An
94/// out-of-range part (month 13) is also "not available", since the standard
95/// defines no meaning for it.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct Eta {
99    /// Month, `1..=12`.
100    pub month: Option<u8>,
101    /// Day, `1..=31`.
102    pub day: Option<u8>,
103    /// Hour, `0..=23`.
104    pub hour: Option<u8>,
105    /// Minute, `0..=59`.
106    pub minute: Option<u8>,
107}
108
109impl Eta {
110    /// ETA with no part given.
111    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    /// Whether any part is given.
128    #[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    /// Formats as `MM-DD HH:MM`, `??` for missing parts.
136    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/// Class B static data report: message 24, sent as two parts.
152#[derive(Debug, Clone, Copy, PartialEq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct StaticDataReport {
155    /// MMSI.
156    pub mmsi: TargetId,
157    /// Part and its content.
158    pub part: StaticDataPart,
159}
160
161impl StaticDataReport {
162    /// Part A: 160 bits; part B: 168.
163    const PART_A_NEEDED: usize = 160;
164    const PART_B_NEEDED: usize = 168;
165
166    /// `None` for a reserved part number.
167    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/// Parts of a class B static data report.
188///
189/// `#[non_exhaustive]`; match with a wildcard arm.
190#[non_exhaustive]
191#[derive(Debug, Clone, Copy, PartialEq)]
192#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
193pub enum StaticDataPart {
194    /// Part A: name.
195    A {
196        /// Name, up to 20 characters.
197        name: Option<InlineStr<NAME_CHARS>>,
198    },
199    /// Part B: remaining static data.
200    B(ClassBStaticData),
201}
202
203/// Class B static data, part B.
204#[derive(Debug, Clone, Copy, PartialEq)]
205#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
206pub struct ClassBStaticData {
207    /// Ship type.
208    pub ship_type: Option<ShipType>,
209    /// Vendor ID, 3 characters.
210    pub vendor_id: Option<InlineStr<VENDOR_CHARS>>,
211    /// Vendor model code, `0..=15`.
212    pub unit_model: u8,
213    /// Vendor serial number, 20 bits.
214    pub serial_number: u32,
215    /// Call sign, up to 7 characters.
216    pub callsign: Option<InlineStr<CALLSIGN_CHARS>>,
217    /// Dimensions around the reported position; for an auxiliary craft this
218    /// field carries the mothership MMSI instead.
219    pub dimensions: Option<Dimensions>,
220    /// Mothership MMSI, for an auxiliary craft (MMSI starting 98).
221    pub mother_ship: Option<TargetId>,
222    /// Position fixing device.
223    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
251/// Whether an MMSI is an auxiliary craft of a parent ship: `98`, MID, four
252/// digits.
253const 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    // Draught in 0.1 m.
266    Distance::from_metres(f64::from(field) / counts_per::TENTH)
267        .map(Some)
268        .map_err(value_of("draught"))
269}