use crate::bits::{BitReader, BitWriter};
use crate::error::BitError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BinaryPayload<const N: usize> {
buf: [u8; N],
bit_len: usize,
}
impl<const N: usize> BinaryPayload<N> {
#[must_use]
pub const fn bit_len(&self) -> usize {
self.bit_len
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.buf[..self.bit_len.div_ceil(8)]
}
pub(crate) fn decode(r: &mut BitReader<'_>, bit_len: usize) -> Result<Self, BitError> {
if bit_len.div_ceil(8) > N {
return Err(BitError::OutOfRange {
field: "BinaryPayload",
});
}
let mut buf = [0u8; N];
let mut remaining = bit_len;
let mut i = 0;
while remaining > 0 {
let take = remaining.min(8);
#[allow(
clippy::cast_possible_truncation,
reason = "take is clamped to 8 above, fits u32"
)]
let byte = r.read_u8(take as u32)?;
buf[i] = byte << (8 - take);
remaining -= take;
i += 1;
}
Ok(Self { buf, bit_len })
}
pub(crate) fn encode(&self, w: &mut BitWriter<'_>) -> Result<(), BitError> {
let mut remaining = self.bit_len;
let mut i = 0;
while remaining > 0 {
let take = remaining.min(8);
#[allow(
clippy::cast_possible_truncation,
reason = "take is clamped to 8 above, fits u32"
)]
let take_u32 = take as u32;
let byte = self.buf[i] >> (8 - take);
w.write_bits(u64::from(byte), take_u32)?;
remaining -= take;
i += 1;
}
Ok(())
}
#[cfg(test)]
pub(crate) const fn test_from_raw(buf: [u8; N], bit_len: usize) -> Self {
Self { buf, bit_len }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Mmsi(u32);
impl Mmsi {
#[must_use]
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
}
impl core::fmt::Display for Mmsi {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:09}", self.0)
}
}
pub const LONGITUDE_NOT_AVAILABLE_RAW: i32 = 181 * 60 * 10_000;
pub const LATITUDE_NOT_AVAILABLE_RAW: i32 = 91 * 60 * 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Longitude(i32);
impl Longitude {
#[must_use]
pub const fn from_raw(raw: i32) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> i32 {
self.0
}
#[must_use]
pub const fn is_available(self) -> bool {
self.0 != LONGITUDE_NOT_AVAILABLE_RAW
}
#[must_use]
pub fn as_degrees(self) -> Option<f64> {
self.is_available().then(|| f64::from(self.0) / 600_000.0)
}
#[cfg(feature = "uom")]
#[must_use]
pub fn to_uom(self) -> Option<uom::si::f64::Angle> {
use uom::si::angle::degree;
Some(uom::si::f64::Angle::new::<degree>(self.as_degrees()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Latitude(i32);
impl Latitude {
#[must_use]
pub const fn from_raw(raw: i32) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> i32 {
self.0
}
#[must_use]
pub const fn is_available(self) -> bool {
self.0 != LATITUDE_NOT_AVAILABLE_RAW
}
#[must_use]
pub fn as_degrees(self) -> Option<f64> {
self.is_available().then(|| f64::from(self.0) / 600_000.0)
}
#[cfg(feature = "uom")]
#[must_use]
pub fn to_uom(self) -> Option<uom::si::f64::Angle> {
use uom::si::angle::degree;
Some(uom::si::f64::Angle::new::<degree>(self.as_degrees()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Position {
pub latitude: f64,
pub longitude: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sog(u16);
impl Sog {
pub const NOT_AVAILABLE_RAW: u16 = 1023;
pub const HIGH_SPEED_RAW: u16 = 1022;
#[must_use]
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> u16 {
self.0
}
#[must_use]
pub const fn is_available(self) -> bool {
self.0 != Self::NOT_AVAILABLE_RAW
}
#[must_use]
pub fn knots(self) -> Option<f64> {
self.is_available().then(|| f64::from(self.0) / 10.0)
}
#[cfg(feature = "uom")]
#[must_use]
pub fn to_uom(self) -> Option<uom::si::f64::Velocity> {
use uom::si::velocity::knot;
Some(uom::si::f64::Velocity::new::<knot>(self.knots()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cog(u16);
impl Cog {
pub const NOT_AVAILABLE_RAW: u16 = 3600;
#[must_use]
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> u16 {
self.0
}
#[must_use]
pub const fn is_available(self) -> bool {
self.0 != Self::NOT_AVAILABLE_RAW
}
#[must_use]
pub fn degrees(self) -> Option<f64> {
self.is_available().then(|| f64::from(self.0) / 10.0)
}
#[cfg(feature = "uom")]
#[must_use]
pub fn to_uom(self) -> Option<uom::si::f64::Angle> {
use uom::si::angle::degree;
Some(uom::si::f64::Angle::new::<degree>(self.degrees()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Heading(u16);
impl Heading {
pub const NOT_AVAILABLE_RAW: u16 = 511;
#[must_use]
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> u16 {
self.0
}
#[must_use]
pub const fn is_available(self) -> bool {
self.0 != Self::NOT_AVAILABLE_RAW
}
#[must_use]
pub const fn degrees(self) -> Option<u16> {
if self.is_available() {
Some(self.0)
} else {
None
}
}
#[cfg(feature = "uom")]
#[must_use]
pub fn to_uom(self) -> Option<uom::si::f64::Angle> {
use uom::si::angle::degree;
Some(uom::si::f64::Angle::new::<degree>(f64::from(
self.degrees()?,
)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RateOfTurn(i8);
impl RateOfTurn {
#[must_use]
pub const fn from_raw(raw: i8) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(self) -> i8 {
self.0
}
#[must_use]
pub const fn is_available(self) -> bool {
self.0 != -128
}
#[must_use]
pub fn degrees_per_minute(self) -> Option<f64> {
if !self.is_available() || self.0 == 127 || self.0 == -127 {
return None;
}
let x = f64::from(self.0) / 4.733;
Some(x * x.abs())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum NavigationStatus {
UnderWayUsingEngine,
AtAnchor,
NotUnderCommand,
RestrictedManoeuvrability,
ConstrainedByDraught,
Moored,
Aground,
EngagedInFishing,
UnderWaySailing,
ReservedHsc,
ReservedWig,
ReservedTowingAstern,
ReservedPushingAhead,
Reserved13,
AisSartMobEpirb,
NotDefined,
}
impl NavigationStatus {
#[must_use]
pub const fn from_raw(v: u8) -> Self {
match v {
0 => Self::UnderWayUsingEngine,
1 => Self::AtAnchor,
2 => Self::NotUnderCommand,
3 => Self::RestrictedManoeuvrability,
4 => Self::ConstrainedByDraught,
5 => Self::Moored,
6 => Self::Aground,
7 => Self::EngagedInFishing,
8 => Self::UnderWaySailing,
9 => Self::ReservedHsc,
10 => Self::ReservedWig,
11 => Self::ReservedTowingAstern,
12 => Self::ReservedPushingAhead,
13 => Self::Reserved13,
14 => Self::AisSartMobEpirb,
_ => Self::NotDefined,
}
}
#[must_use]
pub const fn to_raw(self) -> u8 {
match self {
Self::UnderWayUsingEngine => 0,
Self::AtAnchor => 1,
Self::NotUnderCommand => 2,
Self::RestrictedManoeuvrability => 3,
Self::ConstrainedByDraught => 4,
Self::Moored => 5,
Self::Aground => 6,
Self::EngagedInFishing => 7,
Self::UnderWaySailing => 8,
Self::ReservedHsc => 9,
Self::ReservedWig => 10,
Self::ReservedTowingAstern => 11,
Self::ReservedPushingAhead => 12,
Self::Reserved13 => 13,
Self::AisSartMobEpirb => 14,
Self::NotDefined => 15,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ManeuverIndicator {
NotAvailable,
NoSpecialManeuver,
SpecialManeuver,
Reserved,
}
impl ManeuverIndicator {
#[must_use]
pub const fn from_raw(v: u8) -> Self {
match v {
0 => Self::NotAvailable,
1 => Self::NoSpecialManeuver,
2 => Self::SpecialManeuver,
_ => Self::Reserved,
}
}
#[must_use]
pub const fn to_raw(self) -> u8 {
match self {
Self::NotAvailable => 0,
Self::NoSpecialManeuver => 1,
Self::SpecialManeuver => 2,
Self::Reserved => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Timestamp {
Second(u8),
NotAvailable,
ManualInputMode,
DeadReckoning,
Inoperative,
}
impl Timestamp {
#[must_use]
pub const fn from_raw(v: u8) -> Self {
match v {
0..=59 => Self::Second(v),
61 => Self::ManualInputMode,
62 => Self::DeadReckoning,
63 => Self::Inoperative,
_ => Self::NotAvailable,
}
}
#[must_use]
pub const fn to_raw(self) -> u8 {
match self {
Self::Second(s) => s,
Self::NotAvailable => 60,
Self::ManualInputMode => 61,
Self::DeadReckoning => 62,
Self::Inoperative => 63,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EpfdType {
Undefined,
Gps,
Glonass,
GpsGlonass,
LoranC,
Chayka,
IntegratedNavigationSystem,
Surveyed,
Galileo,
Reserved(u8),
}
impl EpfdType {
#[must_use]
pub const fn from_raw(v: u8) -> Self {
match v {
1 => Self::Gps,
2 => Self::Glonass,
3 => Self::GpsGlonass,
4 => Self::LoranC,
5 => Self::Chayka,
6 => Self::IntegratedNavigationSystem,
7 => Self::Surveyed,
8 => Self::Galileo,
0 => Self::Undefined,
other => Self::Reserved(other),
}
}
#[must_use]
pub const fn to_raw(self) -> u8 {
match self {
Self::Undefined => 0,
Self::Gps => 1,
Self::Glonass => 2,
Self::GpsGlonass => 3,
Self::LoranC => 4,
Self::Chayka => 5,
Self::IntegratedNavigationSystem => 6,
Self::Surveyed => 7,
Self::Galileo => 8,
Self::Reserved(v) => v,
}
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use std::string::ToString;
#[test]
fn mmsi_displays_zero_padded() {
assert_eq!(Mmsi::from_raw(123).to_string(), "000000123");
}
#[cfg(feature = "uom")]
#[test]
fn uom_conversions_match_plain_accessors() {
use uom::si::angle::degree;
use uom::si::velocity::knot;
let sog = Sog::from_raw(35);
assert!((sog.to_uom().unwrap().get::<knot>() - sog.knots().unwrap()).abs() < 1e-9);
assert_eq!(Sog::from_raw(Sog::NOT_AVAILABLE_RAW).to_uom(), None);
let cog = Cog::from_raw(1234);
assert!((cog.to_uom().unwrap().get::<degree>() - cog.degrees().unwrap()).abs() < 1e-9);
let heading = Heading::from_raw(88);
assert!(
(heading.to_uom().unwrap().get::<degree>() - f64::from(heading.degrees().unwrap()))
.abs()
< 1e-9
);
let lon = Longitude::from_raw(-44_100_000);
assert!((lon.to_uom().unwrap().get::<degree>() - lon.as_degrees().unwrap()).abs() < 1e-9);
let lat = Latitude::from_raw(24_600_000);
assert!((lat.to_uom().unwrap().get::<degree>() - lat.as_degrees().unwrap()).abs() < 1e-9);
}
#[test]
fn navigation_status_roundtrips() {
for v in 0u8..16 {
assert_eq!(NavigationStatus::from_raw(v).to_raw(), v);
}
}
#[test]
fn maneuver_indicator_roundtrips() {
for v in 0u8..4 {
assert_eq!(ManeuverIndicator::from_raw(v).to_raw(), v);
}
}
#[test]
fn timestamp_roundtrips() {
for v in 0u8..64 {
assert_eq!(Timestamp::from_raw(v).to_raw(), v);
}
}
#[test]
fn epfd_type_roundtrips() {
for v in 0u8..16 {
assert_eq!(EpfdType::from_raw(v).to_raw(), v);
}
}
#[test]
fn longitude_not_available_sentinel() {
let lon = Longitude::from_raw(LONGITUDE_NOT_AVAILABLE_RAW);
assert!(!lon.is_available());
assert_eq!(lon.as_degrees(), None);
}
#[test]
fn longitude_known_value() {
let lon = Longitude::from_raw(-44_100_000);
assert_eq!(lon.as_degrees(), Some(-73.5));
}
}