use core::fmt;
use crate::error::Nmea2000Error;
use crate::id::{CanId, Pgn};
pub const MAX_PAYLOAD_BYTES: usize = 223;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "StoredFrame", into = "StoredFrame")
)]
pub struct Frame {
pub id: CanId,
data: [u8; 8],
len: u8,
}
impl Frame {
pub fn new(id: CanId, data: &[u8]) -> Result<Self, Nmea2000Error> {
if data.len() > 8 {
return Err(Nmea2000Error::BadFrameLength { len: data.len() });
}
let mut bytes = [0xFF; 8];
for (slot, byte) in bytes.iter_mut().zip(data) {
*slot = *byte;
}
#[allow(clippy::cast_possible_truncation)]
Ok(Self {
id,
data: bytes,
len: data.len() as u8,
})
}
#[must_use]
pub fn data(&self) -> &[u8] {
self.data.get(..usize::from(self.len)).unwrap_or(&[])
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Payload {
pgn: Pgn,
source: u8,
bytes: [u8; MAX_PAYLOAD_BYTES],
len: usize,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredFrame {
id: CanId,
data: [u8; 8],
len: u8,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredFrame> for Frame {
type Error = Nmea2000Error;
fn try_from(stored: StoredFrame) -> Result<Self, Nmea2000Error> {
let data =
stored
.data
.get(..usize::from(stored.len))
.ok_or(Nmea2000Error::BadFrameLength {
len: usize::from(stored.len),
})?;
Self::new(stored.id, data)
}
}
#[cfg(feature = "serde")]
impl From<Frame> for StoredFrame {
fn from(frame: Frame) -> Self {
Self {
id: frame.id,
data: frame.data,
len: frame.len,
}
}
}
impl Payload {
#[must_use]
pub const fn new(pgn: Pgn, source: u8) -> Self {
Self {
pgn,
source,
bytes: [0; MAX_PAYLOAD_BYTES],
len: 0,
}
}
pub fn from_bytes(pgn: Pgn, source: u8, bytes: &[u8]) -> Result<Self, Nmea2000Error> {
let mut payload = Self::new(pgn, source);
payload.append(bytes)?;
Ok(payload)
}
pub fn append(&mut self, bytes: &[u8]) -> Result<(), Nmea2000Error> {
let end = self.len.saturating_add(bytes.len());
let Some(slots) = self.bytes.get_mut(self.len..end) else {
#[allow(clippy::cast_possible_truncation)]
return Err(Nmea2000Error::BadLength {
declared: end.min(255) as u8,
limit: MAX_PAYLOAD_BYTES,
});
};
slots.copy_from_slice(bytes);
self.len = end;
Ok(())
}
pub(crate) fn truncate(&mut self, len: usize) {
if len < self.len {
for byte in self.bytes.iter_mut().skip(len) {
*byte = 0;
}
self.len = len;
}
}
#[must_use]
pub const fn pgn(&self) -> Pgn {
self.pgn
}
#[must_use]
pub const fn source(&self) -> u8 {
self.source
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.bytes.get(..self.len).unwrap_or(&[])
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn u8(&self, offset: usize) -> Option<u8> {
self.bytes().get(offset).copied()
}
#[must_use]
pub fn unsigned(&self, offset: usize, width: usize) -> Option<u64> {
if width > 8 {
return None;
}
let end = offset.checked_add(width)?;
let bytes = self.bytes().get(offset..end)?;
let mut value = 0_u64;
for &byte in bytes.iter().rev() {
value = (value << 8) | u64::from(byte);
}
Some(value)
}
#[must_use]
pub fn signed(&self, offset: usize, width: usize) -> Option<i64> {
if width == 0 {
return None;
}
let raw = self.unsigned(offset, width)?;
let shift = 64 - 8 * width.min(8);
#[allow(clippy::cast_possible_wrap)]
Some(((raw << shift) as i64) >> shift)
}
#[must_use]
pub fn bits(&self, bit: usize, width: usize) -> Option<u32> {
if width > 32 || width == 0 {
return None;
}
let end = bit.checked_add(width)?;
if end > self.len.min(MAX_PAYLOAD_BYTES) * 8 {
return None;
}
let mut value = 0_u32;
for index in (bit..end).rev() {
let byte = self.u8(index / 8)?;
value = (value << 1) | u32::from((byte >> (index % 8)) & 1);
}
Some(value)
}
}
impl fmt::Debug for Payload {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Payload({} from {}, {} bytes)",
self.pgn, self.source, self.len
)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
fn payload(bytes: &[u8]) -> Payload {
Payload::from_bytes(Pgn::new(1), 0, bytes).unwrap()
}
#[test]
fn fields_are_little_endian() {
let payload = payload(&[0x01, 0x02, 0x03, 0x04, 0xFF, 0xFF, 0xFE, 0x7F]);
assert_eq!(payload.u8(0), Some(1));
assert_eq!(payload.unsigned(0, 2), Some(0x0201));
assert_eq!(payload.unsigned(0, 4), Some(0x0403_0201));
assert_eq!(payload.signed(4, 2), Some(-1));
assert_eq!(payload.signed(6, 2), Some(0x7FFE));
assert_eq!(payload.unsigned(0, 8), Some(0x7FFE_FFFF_0403_0201));
assert_eq!(payload.signed(0, 8), Some(0x7FFE_FFFF_0403_0201));
assert_eq!(payload.unsigned(7, 2), None);
assert_eq!(payload.unsigned(0, 9), None);
assert_eq!(payload.signed(0, 0), None);
assert_eq!(payload.u8(8), None);
}
#[test]
fn bit_fields_run_from_the_low_bit_of_the_first_byte() {
let payload = payload(&[0b1011_0110, 0x35]);
assert_eq!(payload.bits(0, 2), Some(2));
assert_eq!(payload.bits(2, 4), Some(13));
assert_eq!(payload.bits(6, 2), Some(2));
assert_eq!(payload.bits(8, 4), Some(5));
assert_eq!(payload.bits(6, 6), Some(22));
assert_eq!(payload.bits(15, 2), None);
assert_eq!(payload.bits(0, 33), None);
assert_eq!(payload.bits(0, 0), None);
}
#[test]
fn a_frame_and_a_payload_refuse_more_than_they_hold() {
let id = CanId::from_parts(2, Pgn::new(1), 0, 0);
assert!(Frame::new(id, &[0; 8]).is_ok());
assert_eq!(
Frame::new(id, &[0; 9]),
Err(Nmea2000Error::BadFrameLength { len: 9 })
);
assert_eq!(Frame::new(id, &[1, 2]).unwrap().data(), &[1, 2]);
let mut payload = Payload::new(Pgn::new(1), 0);
payload.append(&[0xAA; MAX_PAYLOAD_BYTES]).unwrap();
assert_eq!(payload.len(), MAX_PAYLOAD_BYTES);
assert_eq!(
payload.append(&[0]),
Err(Nmea2000Error::BadLength {
declared: 224,
limit: MAX_PAYLOAD_BYTES
})
);
assert_eq!(
payload.len(),
MAX_PAYLOAD_BYTES,
"nothing appended on an error"
);
payload.truncate(3);
assert_eq!(payload.bytes(), &[0xAA; 3]);
assert_eq!(payload.u8(3), None);
assert_eq!(
std::format!("{payload:?}"),
"Payload(PGN 1 from 0, 3 bytes)"
);
}
}