use defmt_or_log::*;
use zerocopy::{FromBytes, Immutable, IntoBytes, Unaligned, little_endian};
use zerocopy_derive::*;
use crate::{
Pdu,
pdu::{CrcError, Response, ValidationError},
};
const MODBUS_CRC: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_MODBUS);
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Unaligned, Immutable, FromBytes)]
#[repr(C)]
pub struct Frame<T> {
unit_id: u8,
pdu: T,
crc: little_endian::U16,
}
impl<T> Frame<T> {
pub fn new(unit_id: u8, pdu: T) -> Self
where
T: Pdu,
{
let mut frame = Self::without_crc(unit_id, pdu);
frame.update_crc();
frame
}
const fn without_crc(unit_id: u8, pdu: T) -> Self {
Frame {
unit_id,
pdu,
crc: little_endian::U16::ZERO,
}
}
pub const fn builder(unit_id: u8) -> FrameBuilder<T>
where
T: Pdu,
{
FrameBuilder::new(unit_id)
}
fn calculate_crc(&self) -> u16
where
T: IntoBytes + Unaligned + Immutable,
{
let bytes = self.as_bytes();
MODBUS_CRC.checksum(&bytes[..bytes.len() - 2])
}
fn update_crc(&mut self)
where
T: IntoBytes + Unaligned + Immutable,
{
self.crc = self.calculate_crc().into();
}
pub fn validate_crc(&self) -> Result<(), CrcError>
where
T: IntoBytes + Unaligned + Immutable,
{
if self.calculate_crc() == self.crc.get() {
Ok(())
} else {
Err(CrcError)
}
}
pub fn unit_id(&self) -> u8 {
self.unit_id
}
pub fn pdu(&self) -> &T {
&self.pdu
}
pub fn into_data<Request>(self, request: &Frame<Request>) -> Result<T::Data, ValidationError>
where
T: Response<Request>,
{
self.validate_crc()?;
if self.unit_id != request.unit_id {
debug!(
"Request unit id 0x{:02X} does not match response unit id 0x{:02X}",
request.unit_id, self.unit_id
);
return Err(ValidationError::UnexpectedResponse);
}
if !self.pdu.matches_request(&request.pdu) {
debug!("Request PDU does not match response PDU");
return Err(ValidationError::UnexpectedResponse);
}
Ok(self.pdu.into_data())
}
pub fn view(&self) -> FrameView<'_>
where
T: IntoBytes + Unaligned + Immutable,
{
FrameView {
buf: self.as_bytes(),
}
}
}
#[derive(Debug)]
pub struct FrameBuilder<T> {
inner: Frame<T>,
}
impl<T: Pdu> FrameBuilder<T> {
pub const fn with_pdu(unit_id: u8, pdu: T) -> Self {
Self {
inner: Frame::without_crc(unit_id, pdu),
}
}
pub const fn new(unit_id: u8) -> Self {
Self::with_pdu(unit_id, T::DEFAULT)
}
pub const fn set_unit_id(&mut self, unit_id: u8) {
self.inner.unit_id = unit_id;
}
pub fn build_ref(&mut self) -> &mut Frame<T> {
self.inner.update_crc();
&mut self.inner
}
pub fn build(mut self) -> Frame<T> {
self.inner.update_crc();
self.inner
}
pub fn pdu(&self) -> &T {
&self.inner.pdu
}
pub fn pdu_mut(&mut self) -> &mut T {
&mut self.inner.pdu
}
}
impl<T: Pdu> Default for FrameBuilder<T> {
fn default() -> Self {
Self::new(0)
}
}
pub struct FrameView<'a> {
buf: &'a [u8],
}
impl<'a> FrameView<'a> {
pub fn try_from_bytes(buf: &'a [u8]) -> Option<Self> {
if (4..=256).contains(&buf.len()) {
Some(Self { buf })
} else {
None
}
}
pub const fn unit_id(&self) -> u8 {
self.buf[0]
}
pub const fn crc(&self) -> u16 {
u16::from_le_bytes([self.buf[self.buf.len() - 2], self.buf[self.buf.len() - 1]])
}
fn calculate_crc(&self) -> u16 {
MODBUS_CRC.checksum(&self.buf[..self.buf.len() - 2])
}
pub fn pdu(self) -> Result<&'a PduView, CrcError> {
self.validate_crc()?;
Ok(
PduView::ref_from_bytes(&self.buf[1..self.buf.len() - 2])
.expect("PduView is Unaligned"),
)
}
pub fn validate_crc(&self) -> Result<(), CrcError> {
if self.calculate_crc() == self.crc() {
Ok(())
} else {
Err(CrcError)
}
}
}
#[derive(Debug, FromBytes, KnownLayout, Immutable, IntoBytes, Unaligned)]
#[repr(C)]
pub struct PduView {
pub function_code: u8,
pub data: [u8],
}
impl PduView {
#[inline]
pub fn parse<T: Pdu>(&self) -> Option<&T> {
if self.function_code == T::FUNCTION_CODE {
T::try_ref_from_bytes(self.as_bytes()).ok()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use hex_literal::hex;
use super::FrameView;
#[test]
fn test_length_validation() {
assert!(FrameView::try_from_bytes(&hex!()).is_none());
assert!(FrameView::try_from_bytes(&hex!("00 00 00")).is_none());
assert!(FrameView::try_from_bytes(&hex!("00 00 00 00")).is_some());
assert!(FrameView::try_from_bytes(&[0; 256]).is_some());
assert!(FrameView::try_from_bytes(&[0; 257]).is_none());
}
}