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