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    /// Access the inner PDU mutably.
137    pub fn pdu_mut(&mut self) -> &mut T {
138        &mut self.inner.pdu
139    }
140}
141
142impl<T: Pdu> Default for FrameBuilder<T> {
143    fn default() -> Self {
144        Self::new(0)
145    }
146}
147
148/// A frame with an unknown PDU type.
149///
150/// ```
151/// # use hex_literal::hex;
152/// # use async_modbus::FrameView;
153/// let frame = FrameView::try_from_bytes(&hex!("01 06 00 04 00 02 49 CA")).unwrap();
154///
155/// assert_eq!(frame.unit_id(), 1);
156/// assert!(frame.validate_crc().is_ok());
157/// ```
158pub struct FrameView<'a> {
159    buf: &'a [u8],
160}
161
162impl<'a> FrameView<'a> {
163    /// Parse a frame from a byte slice. This method does not validate the CRC
164    /// or the PDU contents, only that the frame has a valid length.
165    pub fn try_from_bytes(buf: &'a [u8]) -> Option<Self> {
166        if (4..=256).contains(&buf.len()) {
167            Some(Self { buf })
168        } else {
169            None
170        }
171    }
172
173    /// The unit ID of the frame.
174    pub const fn unit_id(&self) -> u8 {
175        self.buf[0]
176    }
177
178    /// The CRC sent with the frame. This struct does not guarantee that the
179    /// CRC is valid; be sure to check it with [`FrameView::validate_crc`].
180    pub const fn crc(&self) -> u16 {
181        u16::from_le_bytes([self.buf[self.buf.len() - 2], self.buf[self.buf.len() - 1]])
182    }
183
184    fn calculate_crc(&self) -> u16 {
185        MODBUS_CRC.checksum(&self.buf[..self.buf.len() - 2])
186    }
187
188    /// Returns the wrapped PDU if the CRC is valid.
189    pub fn pdu(self) -> Result<&'a PduView, CrcError> {
190        self.validate_crc()?;
191        Ok(
192            PduView::ref_from_bytes(&self.buf[1..self.buf.len() - 2])
193                .expect("PduView is Unaligned"),
194        )
195    }
196
197    /// Validate the CRC of the frame.
198    ///
199    /// Note that [`FrameView::pdu`] validates the CRC before returning
200    /// the PDU, rendering this method unnecessary to call directly in most
201    /// cases.
202    pub fn validate_crc(&self) -> Result<(), CrcError> {
203        if self.calculate_crc() == self.crc() {
204            Ok(())
205        } else {
206            Err(CrcError)
207        }
208    }
209}
210
211/// The most generic Modbus PDU, containing a function code and data payload.
212#[derive(Debug, FromBytes, KnownLayout, Immutable, IntoBytes, Unaligned)]
213#[repr(C)]
214pub struct PduView {
215    /// The function code of the PDU.
216    pub function_code: u8,
217    /// The data payload of the PDU.
218    pub data: [u8],
219}
220
221impl PduView {
222    /// Parse into a concrete PDU type. Will return `None` upon a function code
223    /// or size mismatch.
224    ///
225    /// ```
226    /// # use hex_literal::hex;
227    /// # use async_modbus::{FrameView, PduView};
228    /// # use async_modbus::pdu::response;
229    /// let pdu = FrameView::try_from_bytes(&hex!("01 03 04 00 01 76 3B CC 40"))
230    ///     .unwrap()
231    ///     .pdu()
232    ///     .unwrap();
233    ///
234    /// let read_holdings = pdu.parse::<response::ReadHoldings::<2>>().unwrap();
235    ///
236    /// assert_eq!(*read_holdings.byte_count(), 4);
237    /// assert_eq!(read_holdings.data().map(|d| d.get()), [0x00_01, 0x76_3B]);
238    /// ```
239    #[inline]
240    pub fn parse<T: Pdu>(&self) -> Option<&T> {
241        if self.function_code == T::FUNCTION_CODE {
242            T::try_ref_from_bytes(self.as_bytes()).ok()
243        } else {
244            None
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use hex_literal::hex;
252
253    use super::*;
254
255    #[test]
256    fn test_length_validation() {
257        assert!(FrameView::try_from_bytes(&hex!()).is_none());
258        assert!(FrameView::try_from_bytes(&hex!("00 00 00")).is_none());
259        assert!(FrameView::try_from_bytes(&hex!("00 00 00 00")).is_some());
260        assert!(FrameView::try_from_bytes(&[0; 256]).is_some());
261        assert!(FrameView::try_from_bytes(&[0; 257]).is_none());
262    }
263}