Skip to main content

pamoja_modbus/
adu.rs

1//! The Modbus RTU application data unit: the frame that goes on the wire.
2
3use crate::crc::crc16;
4use crate::error::ModbusError;
5use crate::function::Exception;
6use crate::response::Response;
7
8/// A Modbus RTU frame: a unit address, a PDU, and a trailing CRC.
9///
10/// This is the complete unit of bytes an RTU transmitter puts on the bus and a receiver
11/// pulls off it. [`from_pdu`](Adu::from_pdu) builds one to send by appending the CRC;
12/// [`parse`](Adu::parse) reads one received, verifying the CRC so a frame corrupted in
13/// transit never reaches the application. The frame lives in a fixed buffer, so neither
14/// path allocates.
15///
16/// # Examples
17///
18/// ```
19/// use pamoja_modbus::Adu;
20///
21/// let frame = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03]).unwrap();
22/// assert_eq!(frame.address(), 0x11);
23/// assert_eq!(frame.function_code(), 0x03);
24///
25/// // A receiver validates the same bytes against the CRC they carry.
26/// let received = Adu::parse(frame.as_bytes()).unwrap();
27/// assert_eq!(received.pdu(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
28/// ```
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct Adu {
31    bytes: [u8; Adu::MAX_LEN],
32    len: usize,
33}
34
35impl Adu {
36    /// The largest a Modbus RTU frame may be, in bytes.
37    pub const MAX_LEN: usize = 256;
38
39    // The smallest valid frame: address, function code, and the two CRC bytes.
40    const MIN_LEN: usize = 4;
41
42    // Lays an address, a PDU, and the CRC into a frame. The caller guarantees the PDU
43    // fits, which every internal caller does by construction.
44    pub(crate) fn assemble(address: u8, pdu: &[u8]) -> Adu {
45        let len = 1 + pdu.len() + 2;
46        let mut bytes = [0u8; Self::MAX_LEN];
47        bytes[0] = address;
48        bytes[1..1 + pdu.len()].copy_from_slice(pdu);
49        let crc = crc16(&bytes[..1 + pdu.len()]);
50        bytes[1 + pdu.len()..len].copy_from_slice(&crc.to_le_bytes());
51        Adu { bytes, len }
52    }
53
54    /// Builds a frame for a unit address and PDU, appending the CRC.
55    ///
56    /// # Arguments
57    ///
58    /// * `address` - the unit (slave) address the frame is for.
59    /// * `pdu` - the protocol data unit: a function code followed by its data.
60    ///
61    /// # Returns
62    ///
63    /// The frame ready to send.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`ModbusError::FrameTooLong`] if `pdu` is longer than a PDU may be, so the
68    /// frame would exceed [`MAX_LEN`](Adu::MAX_LEN) bytes.
69    pub fn from_pdu(address: u8, pdu: &[u8]) -> Result<Adu, ModbusError> {
70        if 1 + pdu.len() + 2 > Self::MAX_LEN {
71            return Err(ModbusError::FrameTooLong);
72        }
73        Ok(Self::assemble(address, pdu))
74    }
75
76    /// Parses a received frame, verifying its CRC.
77    ///
78    /// # Arguments
79    ///
80    /// * `bytes` - the raw frame as it came off the wire, CRC included.
81    ///
82    /// # Returns
83    ///
84    /// The validated frame.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`ModbusError::FrameTooShort`] if `bytes` is shorter than a valid frame,
89    /// [`ModbusError::FrameTooLong`] if it is longer than [`MAX_LEN`](Adu::MAX_LEN), or
90    /// [`ModbusError::CrcMismatch`] if the trailing CRC does not match the contents.
91    pub fn parse(bytes: &[u8]) -> Result<Adu, ModbusError> {
92        if bytes.len() < Self::MIN_LEN {
93            return Err(ModbusError::FrameTooShort);
94        }
95        if bytes.len() > Self::MAX_LEN {
96            return Err(ModbusError::FrameTooLong);
97        }
98        let split = bytes.len() - 2;
99        let expected = crc16(&bytes[..split]);
100        let found = u16::from_le_bytes([bytes[split], bytes[split + 1]]);
101        if expected != found {
102            return Err(ModbusError::CrcMismatch { expected, found });
103        }
104        let mut buffer = [0u8; Self::MAX_LEN];
105        buffer[..bytes.len()].copy_from_slice(bytes);
106        Ok(Adu {
107            bytes: buffer,
108            len: bytes.len(),
109        })
110    }
111
112    /// Returns the unit address, the first byte of the frame.
113    ///
114    /// # Returns
115    ///
116    /// The unit (slave) address.
117    pub fn address(&self) -> u8 {
118        self.bytes[0]
119    }
120
121    /// Returns the function code, the first byte of the PDU.
122    ///
123    /// # Returns
124    ///
125    /// The function code. An exception response has its high bit set.
126    pub fn function_code(&self) -> u8 {
127        self.bytes[1]
128    }
129
130    /// Returns the PDU: the frame without its address and CRC.
131    ///
132    /// # Returns
133    ///
134    /// The protocol data unit as a byte slice.
135    pub fn pdu(&self) -> &[u8] {
136        &self.bytes[1..self.len - 2]
137    }
138
139    /// Returns the whole frame, CRC included, ready for the wire.
140    ///
141    /// # Returns
142    ///
143    /// The frame as a byte slice.
144    pub fn as_bytes(&self) -> &[u8] {
145        &self.bytes[..self.len]
146    }
147
148    /// Returns the exception a device reported, if this frame is an exception response.
149    ///
150    /// # Returns
151    ///
152    /// The [`Exception`] if the function code's high bit is set and an exception byte
153    /// follows it, otherwise [`None`] (including for a defined-but-unknown exception code).
154    pub fn exception(&self) -> Option<Exception> {
155        self.response().exception()
156    }
157
158    /// Returns a reader over this frame's PDU for decoding a response.
159    ///
160    /// # Returns
161    ///
162    /// A [`Response`] borrowing the PDU.
163    pub fn response(&self) -> Response<'_> {
164        Response::new(self.pdu())
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn from_pdu_then_parse_round_trips() {
174        let frame = Adu::from_pdu(0x11, &[0x03, 0x00, 0x6B, 0x00, 0x03]).unwrap();
175        let parsed = Adu::parse(frame.as_bytes()).unwrap();
176        assert_eq!(parsed.address(), 0x11);
177        assert_eq!(parsed.function_code(), 0x03);
178        assert_eq!(parsed.pdu(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
179    }
180
181    #[test]
182    fn parse_accepts_the_spec_request_frame() {
183        let parsed = Adu::parse(&[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x76, 0x87]).unwrap();
184        assert_eq!(parsed.address(), 0x11);
185        assert_eq!(parsed.pdu(), &[0x03, 0x00, 0x6B, 0x00, 0x03]);
186    }
187
188    #[test]
189    fn parse_rejects_a_corrupt_crc() {
190        let result = Adu::parse(&[0x11, 0x03, 0x00, 0x6B, 0x00, 0x03, 0x00, 0x00]);
191        assert_eq!(
192            result,
193            Err(ModbusError::CrcMismatch {
194                expected: 0x8776,
195                found: 0x0000
196            })
197        );
198    }
199
200    #[test]
201    fn parse_rejects_a_short_frame() {
202        assert_eq!(
203            Adu::parse(&[0x11, 0x03, 0x76]),
204            Err(ModbusError::FrameTooShort)
205        );
206    }
207
208    #[test]
209    fn parse_rejects_an_oversized_frame() {
210        let frame = [0u8; Adu::MAX_LEN + 1];
211        assert_eq!(Adu::parse(&frame), Err(ModbusError::FrameTooLong));
212    }
213
214    #[test]
215    fn an_exception_response_surfaces_its_code() {
216        // Read holding registers (0x03) refused with illegal data address (0x02).
217        let frame = Adu::from_pdu(0x11, &[0x83, 0x02]).unwrap();
218        let parsed = Adu::parse(frame.as_bytes()).unwrap();
219        assert_eq!(parsed.exception(), Some(Exception::IllegalDataAddress));
220    }
221
222    #[test]
223    fn a_normal_response_has_no_exception() {
224        let frame = Adu::from_pdu(0x11, &[0x03, 0x02, 0x00, 0x64]).unwrap();
225        assert_eq!(frame.exception(), None);
226    }
227
228    #[test]
229    fn a_maximum_length_frame_round_trips() {
230        // The largest PDU is 253 bytes; with the address and CRC that is the 256-byte
231        // maximum RTU frame.
232        let pdu = [0xAB; 253];
233        let frame = Adu::from_pdu(0x01, &pdu).unwrap();
234        assert_eq!(frame.as_bytes().len(), Adu::MAX_LEN);
235        let parsed = Adu::parse(frame.as_bytes()).unwrap();
236        assert_eq!(parsed.pdu(), &pdu[..]);
237    }
238
239    #[test]
240    fn an_exception_bit_without_a_code_is_not_an_exception() {
241        // A reply whose function code has the exception bit set but carries no exception
242        // byte must read as no exception rather than panic.
243        let frame = Adu::from_pdu(0x11, &[0x83]).unwrap();
244        let parsed = Adu::parse(frame.as_bytes()).unwrap();
245        assert_eq!(parsed.exception(), None);
246    }
247}