Skip to main content

async_modbus/
frame.rs

1use zerocopy::{FromBytes, Immutable, IntoBytes, Unaligned, little_endian, try_transmute_ref};
2use zerocopy_derive::*;
3
4use crate::{
5    Pdu,
6    pdu::{CrcError, Response, ValidationError},
7};
8
9/// A complete Modbus frame.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Unaligned, Immutable, FromBytes)]
11#[repr(C)]
12pub struct Frame<T> {
13    unit_id: u8,
14    pdu: T,
15    crc: little_endian::U16,
16}
17
18impl<T> Frame<T> {
19    /// Creates a new frame with the given unit ID and PDU and calculates the
20    /// CRC.
21    pub fn new(unit_id: u8, pdu: T) -> Self
22    where
23        T: Pdu,
24    {
25        let mut frame = Self::without_crc(unit_id, pdu);
26        frame.update_crc();
27        frame
28    }
29
30    const fn without_crc(unit_id: u8, pdu: T) -> Self {
31        Frame {
32            unit_id,
33            pdu,
34            crc: little_endian::U16::ZERO,
35        }
36    }
37
38    /// Creates a new [`FrameBuilder`].
39    pub const fn builder(unit_id: u8) -> FrameBuilder<T>
40    where
41        T: Pdu,
42    {
43        FrameBuilder::new(unit_id)
44    }
45
46    fn calculate_crc(&self) -> u16
47    where
48        T: IntoBytes + Unaligned + Immutable,
49    {
50        let bytes = self.as_bytes();
51        // The last two bytes are the CRC itself
52        crate::crc(&bytes[..bytes.len() - 2])
53    }
54
55    fn update_crc(&mut self)
56    where
57        T: IntoBytes + Unaligned + Immutable,
58    {
59        self.crc = self.calculate_crc().into();
60    }
61
62    /// Validate the frame against the given request, returning the data if valid.
63    pub fn into_data<Request>(self, request: &Frame<Request>) -> Result<T::Data, ValidationError>
64    where
65        T: Response<Request>,
66    {
67        if self.calculate_crc() != self.crc.get() {
68            return Err(ValidationError::Crc(CrcError));
69        }
70
71        if self.unit_id != request.unit_id {
72            return Err(ValidationError::UnexpectedResponse);
73        }
74
75        if !self.pdu.matches_request(&request.pdu) {
76            return Err(ValidationError::UnexpectedResponse);
77        }
78
79        Ok(self.pdu.into_data())
80    }
81
82    /// Upcast the frame to an [`FrameView`].
83    pub fn view(&self) -> FrameView<'_>
84    where
85        T: IntoBytes + Unaligned + Immutable,
86    {
87        FrameView {
88            buf: self.as_bytes(),
89        }
90    }
91}
92
93/// A builder for [`Frame`]s.
94#[derive(Debug)]
95pub struct FrameBuilder<T> {
96    inner: Frame<T>,
97}
98
99impl<T: Pdu> FrameBuilder<T> {
100    /// Creates a new builder with the given unit ID and PDU.
101    ///
102    /// This is different from [`Frame::new`] in that no CRC is calculated at
103    /// this point.
104    pub const fn with_pdu(unit_id: u8, pdu: T) -> Self {
105        Self {
106            // crc is calculated later
107            inner: Frame::without_crc(unit_id, pdu),
108        }
109    }
110
111    /// Creates a new builder with the given unit ID and default PDU value.
112    pub const fn new(unit_id: u8) -> Self {
113        Self::with_pdu(unit_id, T::DEFAULT)
114    }
115
116    /// Changes the unit ID.
117    pub const fn set_unit_id(&mut self, unit_id: u8) {
118        self.inner.unit_id = unit_id;
119    }
120
121    /// Build a frame but don't move it out of the builder, so that the builder
122    /// can be recycled.
123    pub fn build_ref(&mut self) -> &mut Frame<T> {
124        self.inner.update_crc();
125        &mut self.inner
126    }
127
128    /// Build a frame (calculate its CRC) and move it out of the builder.
129    pub fn build(mut self) -> Frame<T> {
130        self.inner.update_crc();
131        self.inner
132    }
133
134    /// Access the inner PDU mutably.
135    pub fn pdu_mut(&mut self) -> &mut T {
136        &mut self.inner.pdu
137    }
138}
139
140impl<T: Pdu> Default for FrameBuilder<T> {
141    fn default() -> Self {
142        Self::new(0)
143    }
144}
145
146/// A frame with an unknown PDU type.
147///
148/// ```
149/// # use hex_literal::hex;
150/// # use async_modbus::FrameView;
151/// let frame = FrameView::try_from_bytes(&hex!("01 06 00 04 00 02 49 CA")).unwrap();
152///
153/// assert_eq!(frame.unit_id(), 1);
154/// assert!(frame.validate_crc().is_ok());
155/// ```
156pub struct FrameView<'a> {
157    buf: &'a [u8],
158}
159
160impl<'a> FrameView<'a> {
161    /// Parse a frame from a byte slice. This method does not validate the CRC
162    /// or the PDU contents, only that the frame has a valid length.
163    pub fn try_from_bytes(buf: &'a [u8]) -> Option<Self> {
164        if (4..=256).contains(&buf.len()) {
165            Some(Self { buf })
166        } else {
167            None
168        }
169    }
170
171    /// The unit ID of the frame.
172    pub const fn unit_id(&self) -> u8 {
173        self.buf[0]
174    }
175
176    /// The CRC sent with the frame. This struct does not guarantee that the
177    /// CRC is valid; be sure to check it with [`FrameView::validate_crc`].
178    pub const fn crc(&self) -> u16 {
179        u16::from_le_bytes([self.buf[self.buf.len() - 2], self.buf[self.buf.len() - 1]])
180    }
181
182    fn calculate_crc(&self) -> u16 {
183        crate::crc(&self.buf[..self.buf.len() - 2])
184    }
185
186    /// Returns the wrapped PDU if the CRC is valid.
187    pub fn pdu(self) -> Result<&'a PduView, CrcError> {
188        self.validate_crc()?;
189        Ok(
190            PduView::ref_from_bytes(&self.buf[1..self.buf.len() - 2])
191                .expect("PduView is Unaligned"),
192        )
193    }
194
195    /// Validate the CRC of the frame.
196    ///
197    /// Note that [`FrameView::pdu`] validates the CRC before returning
198    /// the PDU, rendering this method unnecessary to call directly in most
199    /// cases.
200    pub fn validate_crc(&self) -> Result<(), CrcError> {
201        if self.calculate_crc() == self.crc() {
202            Ok(())
203        } else {
204            Err(CrcError)
205        }
206    }
207}
208
209/// The most generic Modbus PDU, containing a function code and data payload.
210#[derive(Debug, FromBytes, KnownLayout, Immutable, IntoBytes, Unaligned)]
211#[repr(C)]
212pub struct PduView {
213    /// The function code of the PDU.
214    pub function_code: u8,
215    /// The data payload of the PDU.
216    pub data: [u8],
217}
218
219impl PduView {
220    /// Parse into a concrete PDU type. Will return `None` upon a function code
221    /// or size mismatch.
222    #[inline]
223    pub fn parse<T: Pdu>(&self) -> Option<&T> {
224        if self.function_code == T::FUNCTION_CODE {
225            try_transmute_ref!(self).ok()
226        } else {
227            None
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use hex_literal::hex;
235
236    use super::*;
237
238    #[test]
239    fn test_length_validation() {
240        assert!(FrameView::try_from_bytes(&hex!()).is_none());
241        assert!(FrameView::try_from_bytes(&hex!("00 00 00")).is_none());
242        assert!(FrameView::try_from_bytes(&hex!("00 00 00 00")).is_some());
243        assert!(FrameView::try_from_bytes(&[0; 256]).is_some());
244        assert!(FrameView::try_from_bytes(&[0; 257]).is_none());
245    }
246}