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