Skip to main content

async_modbus/
frame.rs

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