Skip to main content

kinavis_ais/
message.rs

1//! Message decoding into kernel types.
2
3use core::fmt;
4
5use kinavis_kernel::angle::TrueCourse;
6use kinavis_kernel::event::TargetId;
7use kinavis_kernel::position::Position;
8use kinavis_kernel::snapshot::GroundTrack;
9use kinavis_kernel::units::{RateOfTurn, Speed};
10use kinavis_kernel::InlineStr;
11
12use crate::aton::AidToNavigation;
13use crate::bits::Bits;
14use crate::error::AisError;
15use crate::fields::{byte, course, heading, position, second, speed, value_of, Fields};
16use crate::static_data::{StaticAndVoyageData, StaticDataReport};
17use crate::vessel::{Dimensions, PositionFixingDevice, ShipType};
18
19/// Decoded AIS message.
20///
21/// `#[non_exhaustive]`; match with a wildcard arm.
22#[non_exhaustive]
23#[derive(Debug, Clone, Copy, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum Message {
26    /// Position report: messages 1, 2, 3 (class A), 18, 19 (class B).
27    PositionReport(PositionReport),
28    /// Class A static and voyage data: message 5.
29    StaticAndVoyageData(StaticAndVoyageData),
30    /// Class B static data: message 24, part A or B.
31    StaticDataReport(StaticDataReport),
32    /// Aid to navigation: message 21.
33    AidToNavigation(AidToNavigation),
34    /// Well-formed message of an unsupported type, for counting or logging.
35    Unsupported {
36        /// Message type, `0..=63`.
37        kind: u8,
38    },
39}
40
41impl Message {
42    /// Decodes a message from its bits.
43    ///
44    /// # Errors
45    ///
46    /// [`AisError::TooShort`] if the bits end before a field of the type;
47    /// [`AisError::Value`] if a field value is outside the domain.
48    pub fn decode(bits: &Bits) -> Result<Self, AisError> {
49        let kind = bits.unsigned(0, 6).ok_or(AisError::TooShort {
50            bits: bits.len(),
51            needed: 6,
52        })?;
53        let kind = byte(kind);
54        match kind {
55            1..=3 => PositionReport::class_a(bits, kind).map(Self::PositionReport),
56            5 => StaticAndVoyageData::decode(bits).map(Self::StaticAndVoyageData),
57            18 => PositionReport::class_b(bits, ClassB::STANDARD).map(Self::PositionReport),
58            19 => PositionReport::class_b(bits, ClassB::EXTENDED).map(Self::PositionReport),
59            21 => AidToNavigation::decode(bits).map(Self::AidToNavigation),
60            // A reserved part number is an undefined message.
61            24 => Ok(StaticDataReport::decode(bits)?
62                .map_or(Self::Unsupported { kind }, Self::StaticDataReport)),
63            _ => Ok(Self::Unsupported { kind }),
64        }
65    }
66
67    /// Source MMSI; `None` for unsupported messages (not decoded).
68    #[must_use]
69    pub const fn mmsi(&self) -> Option<TargetId> {
70        match self {
71            Self::PositionReport(report) => Some(report.mmsi),
72            Self::StaticAndVoyageData(data) => Some(data.mmsi),
73            Self::StaticDataReport(report) => Some(report.mmsi),
74            Self::AidToNavigation(aid) => Some(aid.mmsi),
75            Self::Unsupported { .. } => None,
76        }
77    }
78}
79
80/// Station class.
81///
82/// `#[non_exhaustive]`; match with a wildcard arm.
83#[non_exhaustive]
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum StationClass {
87    /// Class A: SOLAS carriage requirement; reports navigational status and
88    /// rate of turn.
89    A,
90    /// Class B: reports neither.
91    B,
92}
93
94impl fmt::Display for StationClass {
95    /// Formats as `class A` or `class B`.
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(match self {
98            Self::A => "class A",
99            Self::B => "class B",
100        })
101    }
102}
103
104/// Class A navigational status.
105///
106/// `#[non_exhaustive]`; match with a wildcard arm.
107#[non_exhaustive]
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub enum NavigationStatus {
111    /// Under way using engine.
112    UnderWayUsingEngine,
113    /// At anchor.
114    AtAnchor,
115    /// Not under command.
116    NotUnderCommand,
117    /// Restricted in ability to manoeuvre.
118    RestrictedManoeuvrability,
119    /// Constrained by draught.
120    ConstrainedByDraught,
121    /// Moored.
122    Moored,
123    /// Aground.
124    Aground,
125    /// Engaged in fishing.
126    Fishing,
127    /// Under way sailing.
128    UnderWaySailing,
129    /// Power-driven vessel towing astern.
130    TowingAstern,
131    /// Power-driven vessel pushing ahead or towing alongside.
132    PushingAheadOrTowingAlongside,
133    /// AIS-SART, MOB-AIS or EPIRB-AIS active.
134    SearchAndRescueTransmitter,
135    /// Reserved code, kept as received.
136    Reserved(u8),
137}
138
139impl NavigationStatus {
140    /// Status for a code; `None` for 15 ("not defined").
141    #[must_use]
142    pub const fn from_code(code: u8) -> Option<Self> {
143        Some(match code {
144            0 => Self::UnderWayUsingEngine,
145            1 => Self::AtAnchor,
146            2 => Self::NotUnderCommand,
147            3 => Self::RestrictedManoeuvrability,
148            4 => Self::ConstrainedByDraught,
149            5 => Self::Moored,
150            6 => Self::Aground,
151            7 => Self::Fishing,
152            8 => Self::UnderWaySailing,
153            11 => Self::TowingAstern,
154            12 => Self::PushingAheadOrTowingAlongside,
155            14 => Self::SearchAndRescueTransmitter,
156            9 | 10 | 13 => Self::Reserved(code),
157            _ => return None,
158        })
159    }
160
161    /// Code for the status.
162    #[must_use]
163    pub const fn code(self) -> u8 {
164        match self {
165            Self::UnderWayUsingEngine => 0,
166            Self::AtAnchor => 1,
167            Self::NotUnderCommand => 2,
168            Self::RestrictedManoeuvrability => 3,
169            Self::ConstrainedByDraught => 4,
170            Self::Moored => 5,
171            Self::Aground => 6,
172            Self::Fishing => 7,
173            Self::UnderWaySailing => 8,
174            Self::TowingAstern => 11,
175            Self::PushingAheadOrTowingAlongside => 12,
176            Self::SearchAndRescueTransmitter => 14,
177            Self::Reserved(code) => code,
178        }
179    }
180}
181
182impl fmt::Display for NavigationStatus {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match self {
185            Self::UnderWayUsingEngine => f.write_str("under way using engine"),
186            Self::AtAnchor => f.write_str("at anchor"),
187            Self::NotUnderCommand => f.write_str("not under command"),
188            Self::RestrictedManoeuvrability => f.write_str("restricted manoeuvrability"),
189            Self::ConstrainedByDraught => f.write_str("constrained by draught"),
190            Self::Moored => f.write_str("moored"),
191            Self::Aground => f.write_str("aground"),
192            Self::Fishing => f.write_str("engaged in fishing"),
193            Self::UnderWaySailing => f.write_str("under way sailing"),
194            Self::TowingAstern => f.write_str("towing astern"),
195            Self::PushingAheadOrTowingAlongside => f.write_str("pushing ahead or towing alongside"),
196            Self::SearchAndRescueTransmitter => f.write_str("search and rescue transmitter"),
197            Self::Reserved(code) => write!(f, "reserved status {code}"),
198        }
199    }
200}
201
202/// Class A rate of turn.
203///
204/// The field encodes a scaled square root of the rate: fine resolution for slow
205/// turns, coarse for fast ones; above 5°/s only the direction is given.
206///
207/// `#[non_exhaustive]`; match with a wildcard arm.
208#[non_exhaustive]
209#[derive(Debug, Clone, Copy, PartialEq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211pub enum Turn {
212    /// Rate from the turn indicator.
213    Rate(RateOfTurn),
214    /// Faster than 5°/s (field saturated).
215    OffScale {
216        /// Turning to port; otherwise starboard.
217        to_port: bool,
218    },
219}
220
221impl Turn {
222    /// Field scale: `ROT_AIS = 4.733 √ROT`, ROT in °/min.
223    const SCALE: f64 = 4.733;
224
225    /// Turn for a field value; `None` for `-128` ("not available").
226    fn from_field(value: i32) -> Result<Option<Self>, AisError> {
227        Ok(Some(match value {
228            -128 => return Ok(None),
229            127 => Self::OffScale { to_port: false },
230            -127 => Self::OffScale { to_port: true },
231            _ => {
232                let scaled = f64::from(value) / Self::SCALE;
233                let magnitude = scaled * scaled;
234                let rate = if value < 0 { -magnitude } else { magnitude };
235                Self::Rate(
236                    RateOfTurn::from_degrees_per_minute(rate).map_err(value_of("rate of turn"))?,
237                )
238            }
239        }))
240    }
241}
242
243/// Position report.
244///
245/// Every field that can be "not available" is an `Option`, `None` when
246/// unavailable — never a zero that reads as a valid course or a stopped ship.
247#[derive(Debug, Clone, Copy, PartialEq)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub struct PositionReport {
250    /// Message type: 1, 2 or 3 (class A), 18 or 19 (class B).
251    pub kind: u8,
252    /// MMSI.
253    pub mmsi: TargetId,
254    /// Navigational status; class A only.
255    pub status: Option<NavigationStatus>,
256    /// Rate of turn; class A only.
257    pub turn: Option<Turn>,
258    /// Speed over ground, 0.1 kn resolution. 102.2 kn means 102.2 kn or more.
259    pub speed: Option<Speed>,
260    /// Position accuracy flag: DGNSS, better than 10 m.
261    pub accurate: bool,
262    /// Position, 1/10 000 minute resolution.
263    pub position: Option<Position>,
264    /// Course over ground, 0.1° resolution.
265    pub course: Option<TrueCourse>,
266    /// True heading, 1° resolution.
267    pub heading: Option<TrueCourse>,
268    /// UTC second of the report, `0..=59`; `None` if no time is available or
269    /// the position is dead-reckoned or manual.
270    pub second: Option<u8>,
271    /// RAIM flag.
272    pub raim: bool,
273    /// Name, up to 20 characters; message 19 only (others use static data).
274    pub name: Option<InlineStr<20>>,
275    /// Ship type; message 19 only.
276    pub ship_type: Option<ShipType>,
277    /// Dimensions around the position; message 19 only.
278    pub dimensions: Option<Dimensions>,
279    /// Position fixing device; message 19 only.
280    pub fixing_device: Option<PositionFixingDevice>,
281}
282
283/// Class B field layout; messages 18 and 19 differ only at the end.
284#[derive(Clone, Copy)]
285struct ClassB {
286    needed: usize,
287    raim: usize,
288}
289
290impl ClassB {
291    const STANDARD: Self = Self {
292        needed: 168,
293        raim: 147,
294    };
295    const EXTENDED: Self = Self {
296        needed: 312,
297        raim: 305,
298    };
299}
300
301impl PositionReport {
302    /// Messages 1, 2 and 3.
303    fn class_a(bits: &Bits, kind: u8) -> Result<Self, AisError> {
304        let field = Fields::of(bits, 168)?;
305        Ok(Self {
306            kind,
307            mmsi: TargetId::new(field.unsigned(8, 30)),
308            status: NavigationStatus::from_code(byte(field.unsigned(38, 4))),
309            turn: Turn::from_field(field.signed(42, 8))?,
310            speed: speed(field.unsigned(50, 10))?,
311            accurate: field.bit(60),
312            position: position(field.signed(61, 28), field.signed(89, 27))?,
313            course: course(field.unsigned(116, 12))?,
314            heading: heading(field.unsigned(128, 9))?,
315            second: second(field.unsigned(137, 6)),
316            raim: field.bit(148),
317            name: None,
318            ship_type: None,
319            dimensions: None,
320            fixing_device: None,
321        })
322    }
323
324    /// Messages 18 and 19.
325    fn class_b(bits: &Bits, layout: ClassB) -> Result<Self, AisError> {
326        let field = Fields::of(bits, layout.needed)?;
327        let extended = layout.needed == ClassB::EXTENDED.needed;
328        Ok(Self {
329            kind: byte(field.unsigned(0, 6)),
330            mmsi: TargetId::new(field.unsigned(8, 30)),
331            status: None,
332            turn: None,
333            speed: speed(field.unsigned(46, 10))?,
334            accurate: field.bit(56),
335            position: position(field.signed(57, 28), field.signed(85, 27))?,
336            course: course(field.unsigned(112, 12))?,
337            heading: heading(field.unsigned(124, 9))?,
338            second: second(field.unsigned(133, 6)),
339            raim: field.bit(layout.raim),
340            name: extended.then(|| field.text(143, 20)).flatten(),
341            ship_type: extended
342                .then(|| ShipType::from_code(byte(field.unsigned(263, 8))))
343                .flatten(),
344            dimensions: if extended {
345                Dimensions::from_fields(
346                    field.unsigned(271, 9),
347                    field.unsigned(280, 9),
348                    field.unsigned(289, 6),
349                    field.unsigned(295, 6),
350                )?
351            } else {
352                None
353            },
354            fixing_device: extended
355                .then(|| PositionFixingDevice::from_code(byte(field.unsigned(301, 4))))
356                .flatten(),
357        })
358    }
359
360    /// Station class.
361    #[must_use]
362    pub const fn station_class(&self) -> StationClass {
363        match self.kind {
364            18 | 19 => StationClass::B,
365            _ => StationClass::A,
366        }
367    }
368
369    /// Course and speed over ground, if both are reported.
370    #[must_use]
371    pub fn ground_track(&self) -> Option<GroundTrack> {
372        Some(GroundTrack {
373            course_over_ground: self.course?,
374            speed_over_ground: self.speed?,
375        })
376    }
377}