use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Pgn(u32);
impl Pgn {
pub const POSITION_RAPID_UPDATE: Self = Self(129_025);
pub const COG_SOG_RAPID_UPDATE: Self = Self(129_026);
pub const GNSS_POSITION_DATA: Self = Self(129_029);
pub const VESSEL_HEADING: Self = Self(127_250);
pub const WATER_DEPTH: Self = Self(128_267);
pub const AIS_CLASS_A_POSITION_REPORT: Self = Self(129_038);
pub const AIS_CLASS_B_POSITION_REPORT: Self = Self(129_039);
#[must_use]
pub const fn new(number: u32) -> Self {
Self(number)
}
#[must_use]
pub const fn number(self) -> u32 {
self.0
}
#[must_use]
pub const fn transport(self) -> Option<Transport> {
match self {
Self::POSITION_RAPID_UPDATE
| Self::COG_SOG_RAPID_UPDATE
| Self::VESSEL_HEADING
| Self::WATER_DEPTH => Some(Transport::SingleFrame),
Self::GNSS_POSITION_DATA
| Self::AIS_CLASS_A_POSITION_REPORT
| Self::AIS_CLASS_B_POSITION_REPORT => Some(Transport::FastPacket),
_ => None,
}
}
}
impl fmt::Display for Pgn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PGN {}", self.0)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Transport {
SingleFrame,
FastPacket,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CanId(u32);
impl CanId {
pub const fn new(raw: u32) -> Result<Self, crate::Nmea2000Error> {
if raw >> 29 != 0 {
return Err(crate::Nmea2000Error::BadCanId { raw });
}
Ok(Self(raw))
}
#[must_use]
pub const fn from_parts(priority: u8, pgn: Pgn, destination: u8, source: u8) -> Self {
let pgn = pgn.0 & 0x3_FFFF;
let pf = (pgn >> 8) & 0xFF;
let ps = if pf < 240 {
destination as u32
} else {
pgn & 0xFF
};
Self(
((priority as u32 & 0x7) << 26)
| ((pgn >> 16) << 24)
| (pf << 16)
| (ps << 8)
| source as u32,
)
}
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
#[must_use]
pub const fn priority(self) -> u8 {
((self.0 >> 26) & 0x7) as u8
}
#[must_use]
pub const fn pgn(self) -> Pgn {
let bits = (self.0 >> 8) & 0x3_FFFF;
if Self::is_pdu1(bits) {
Pgn(bits & 0x3_FF00)
} else {
Pgn(bits)
}
}
#[must_use]
pub const fn destination(self) -> Option<u8> {
let bits = (self.0 >> 8) & 0x3_FFFF;
if Self::is_pdu1(bits) {
Some((bits & 0xFF) as u8)
} else {
None
}
}
#[must_use]
pub const fn source(self) -> u8 {
(self.0 & 0xFF) as u8
}
const fn is_pdu1(pgn_bits: u32) -> bool {
(pgn_bits >> 8) & 0xFF < 240
}
}
impl fmt::Display for CanId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} from {}", self.pgn(), self.source())?;
if let Some(destination) = self.destination() {
write!(f, " to {destination}")?;
}
write!(f, ", priority {}", self.priority())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn a_broadcast_group_takes_the_whole_low_word() {
let id = CanId::new(0x09F8_0123).unwrap();
assert_eq!(id.priority(), 2);
assert_eq!(id.pgn(), Pgn::POSITION_RAPID_UPDATE);
assert_eq!(id.destination(), None);
assert_eq!(id.source(), 35);
assert_eq!(id, CanId::from_parts(2, Pgn::POSITION_RAPID_UPDATE, 0, 35));
assert_eq!(std::format!("{id}"), "PGN 129025 from 35, priority 2");
}
#[test]
fn an_addressed_group_keeps_its_destination_out_of_the_number() {
let id = CanId::new((6 << 26) | (0xEA << 16) | (0x0C << 8) | 0x21).unwrap();
assert_eq!(id.pgn(), Pgn::new(59_904));
assert_eq!(id.destination(), Some(12));
assert_eq!(id.source(), 0x21);
assert_eq!(id, CanId::from_parts(6, Pgn::new(59_904), 12, 0x21));
assert_eq!(std::format!("{id}"), "PGN 59904 from 33 to 12, priority 6");
}
#[test]
fn the_data_page_bit_reaches_the_group_number() {
let id = CanId::from_parts(2, Pgn::new(130_306), 0, 1);
assert_eq!(id.pgn().number(), 130_306);
assert_eq!(id.raw() >> 24 & 0x1, 1);
}
#[test]
fn thirty_bits_are_refused() {
assert_eq!(
CanId::new(1 << 29),
Err(crate::Nmea2000Error::BadCanId { raw: 1 << 29 })
);
assert!(CanId::new((1 << 29) - 1).is_ok());
}
#[test]
fn the_known_groups_know_their_transport() {
assert_eq!(Pgn::WATER_DEPTH.transport(), Some(Transport::SingleFrame));
assert_eq!(
Pgn::GNSS_POSITION_DATA.transport(),
Some(Transport::FastPacket)
);
assert_eq!(Pgn::new(130_306).transport(), None);
assert_eq!(std::format!("{}", Pgn::WATER_DEPTH), "PGN 128267");
}
}