Skip to main content

async_modbus/
frame.rs

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