1use 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#[non_exhaustive]
23#[derive(Debug, Clone, Copy, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum Message {
26 PositionReport(PositionReport),
28 StaticAndVoyageData(StaticAndVoyageData),
30 StaticDataReport(StaticDataReport),
32 AidToNavigation(AidToNavigation),
34 Unsupported {
36 kind: u8,
38 },
39}
40
41impl Message {
42 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 24 => Ok(StaticDataReport::decode(bits)?
62 .map_or(Self::Unsupported { kind }, Self::StaticDataReport)),
63 _ => Ok(Self::Unsupported { kind }),
64 }
65 }
66
67 #[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#[non_exhaustive]
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub enum StationClass {
87 A,
90 B,
92}
93
94impl fmt::Display for StationClass {
95 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#[non_exhaustive]
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub enum NavigationStatus {
111 UnderWayUsingEngine,
113 AtAnchor,
115 NotUnderCommand,
117 RestrictedManoeuvrability,
119 ConstrainedByDraught,
121 Moored,
123 Aground,
125 Fishing,
127 UnderWaySailing,
129 TowingAstern,
131 PushingAheadOrTowingAlongside,
133 SearchAndRescueTransmitter,
135 Reserved(u8),
137}
138
139impl NavigationStatus {
140 #[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 #[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#[non_exhaustive]
209#[derive(Debug, Clone, Copy, PartialEq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211pub enum Turn {
212 Rate(RateOfTurn),
214 OffScale {
216 to_port: bool,
218 },
219}
220
221impl Turn {
222 const SCALE: f64 = 4.733;
224
225 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#[derive(Debug, Clone, Copy, PartialEq)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub struct PositionReport {
250 pub kind: u8,
252 pub mmsi: TargetId,
254 pub status: Option<NavigationStatus>,
256 pub turn: Option<Turn>,
258 pub speed: Option<Speed>,
260 pub accurate: bool,
262 pub position: Option<Position>,
264 pub course: Option<TrueCourse>,
266 pub heading: Option<TrueCourse>,
268 pub second: Option<u8>,
271 pub raim: bool,
273 pub name: Option<InlineStr<20>>,
275 pub ship_type: Option<ShipType>,
277 pub dimensions: Option<Dimensions>,
279 pub fixing_device: Option<PositionFixingDevice>,
281}
282
283#[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 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 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 #[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 #[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}